@lyrify/znl 0.4.1 → 0.5.2
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 +38 -8
- package/package.json +14 -2
- package/src/SendQueue.js +51 -17
- package/src/ZNL.js +657 -83
- package/src/constants.js +33 -0
- package/src/protocol.js +74 -12
- package/src/security.js +578 -0
package/src/ZNL.js
CHANGED
|
@@ -13,10 +13,15 @@
|
|
|
13
13
|
* auth_failed / slave_connected / slave_disconnected
|
|
14
14
|
* publish / error
|
|
15
15
|
*
|
|
16
|
+
* 加密模式(encrypted):
|
|
17
|
+
* - false : 明文模式(不签名/不加密)
|
|
18
|
+
* - true : 签名 + 防重放 + payload 透明加密(AES-256-GCM)
|
|
19
|
+
*
|
|
16
20
|
* 心跳机制:
|
|
17
21
|
* - slave 每隔 heartbeatInterval ms 向 master 发送一帧心跳
|
|
18
22
|
* - master 每隔 heartbeatInterval ms 扫描一次在线列表
|
|
19
|
-
* - 超过
|
|
23
|
+
* - 超过 heartbeatTimeoutMs 未收到心跳,判定 slave 已崩溃并移除
|
|
24
|
+
* (heartbeatTimeoutMs <= 0 时,默认按 heartbeatInterval × 3 计算)
|
|
20
25
|
*/
|
|
21
26
|
|
|
22
27
|
import { randomUUID } from "node:crypto";
|
|
@@ -26,9 +31,16 @@ import * as zmq from "zeromq";
|
|
|
26
31
|
import {
|
|
27
32
|
DEFAULT_ENDPOINTS,
|
|
28
33
|
DEFAULT_HEARTBEAT_INTERVAL,
|
|
34
|
+
DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
35
|
+
DEFAULT_MAX_PENDING,
|
|
36
|
+
DEFAULT_ENABLE_PAYLOAD_DIGEST,
|
|
29
37
|
CONTROL_PREFIX,
|
|
30
38
|
CONTROL_HEARTBEAT,
|
|
39
|
+
SECURITY_ENVELOPE_VERSION,
|
|
40
|
+
MAX_TIME_SKEW_MS,
|
|
41
|
+
REPLAY_WINDOW_MS,
|
|
31
42
|
} from "./constants.js";
|
|
43
|
+
|
|
32
44
|
import {
|
|
33
45
|
identityToString,
|
|
34
46
|
identityToBuffer,
|
|
@@ -36,11 +48,27 @@ import {
|
|
|
36
48
|
payloadFromFrames,
|
|
37
49
|
buildRegisterFrames,
|
|
38
50
|
buildUnregisterFrames,
|
|
51
|
+
buildHeartbeatFrames,
|
|
39
52
|
buildRequestFrames,
|
|
40
53
|
buildResponseFrames,
|
|
41
54
|
buildPublishFrames,
|
|
42
55
|
parseControlFrames,
|
|
43
56
|
} from "./protocol.js";
|
|
57
|
+
|
|
58
|
+
import {
|
|
59
|
+
ReplayGuard,
|
|
60
|
+
deriveKeys,
|
|
61
|
+
toFrameBuffers,
|
|
62
|
+
digestFrames,
|
|
63
|
+
generateNonce,
|
|
64
|
+
nowMs,
|
|
65
|
+
canonicalSignInput,
|
|
66
|
+
encodeAuthProofToken,
|
|
67
|
+
decodeAuthProofToken,
|
|
68
|
+
encryptFrames,
|
|
69
|
+
decryptFrames,
|
|
70
|
+
} from "./security.js";
|
|
71
|
+
|
|
44
72
|
import { PendingManager } from "./PendingManager.js";
|
|
45
73
|
import { SendQueue } from "./SendQueue.js";
|
|
46
74
|
|
|
@@ -49,14 +77,18 @@ export class ZNL extends EventEmitter {
|
|
|
49
77
|
|
|
50
78
|
/** @type {"master"|"slave"} */
|
|
51
79
|
role;
|
|
80
|
+
|
|
52
81
|
/** @type {string} 节点唯一标识,slave 侧同时作为 ZMQ routingId */
|
|
53
82
|
id;
|
|
83
|
+
|
|
54
84
|
/** @type {{ router: string }} */
|
|
55
85
|
endpoints;
|
|
56
|
-
|
|
86
|
+
|
|
87
|
+
/** @type {string} 共享认证 Key(仅 encrypted=true 使用) */
|
|
57
88
|
authKey;
|
|
58
|
-
|
|
59
|
-
|
|
89
|
+
|
|
90
|
+
/** @type {boolean} 是否启用加密安全(签名+防重放+透明加密) */
|
|
91
|
+
encrypted;
|
|
60
92
|
|
|
61
93
|
// ─── 运行状态 ─────────────────────────────────────────────────────────────
|
|
62
94
|
|
|
@@ -89,12 +121,18 @@ export class ZNL extends EventEmitter {
|
|
|
89
121
|
/** @type {Function|null} slave 侧收到 master 请求时的自动回复处理器 */
|
|
90
122
|
#dealerAutoHandler = null;
|
|
91
123
|
|
|
124
|
+
/** @type {number} 心跳超时时间(ms),0 表示使用 interval × 3 */
|
|
125
|
+
#heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS;
|
|
126
|
+
|
|
127
|
+
/** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
|
|
128
|
+
#enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
|
|
129
|
+
|
|
92
130
|
// ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
|
|
93
131
|
|
|
94
132
|
/**
|
|
95
133
|
* master 侧:已注册的在线 slave 表
|
|
96
134
|
* key = slaveId(字符串)
|
|
97
|
-
* value = { identity: Buffer
|
|
135
|
+
* value = { identity: Buffer, lastSeen: number }
|
|
98
136
|
* @type {Map<string, { identity: Buffer, lastSeen: number }>}
|
|
99
137
|
*/
|
|
100
138
|
#slaves = new Map();
|
|
@@ -116,27 +154,57 @@ export class ZNL extends EventEmitter {
|
|
|
116
154
|
/** master 侧扫描死节点的定时器句柄 */
|
|
117
155
|
#heartbeatCheckTimer = null;
|
|
118
156
|
|
|
157
|
+
// ─── 安全运行时状态 ───────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
/** @type {boolean} 当前是否启用签名安全(auth / encrypted) */
|
|
160
|
+
#secureEnabled = false;
|
|
161
|
+
|
|
162
|
+
/** @type {Buffer|null} HMAC 签名密钥(由 authKey HKDF 派生) */
|
|
163
|
+
#signKey = null;
|
|
164
|
+
|
|
165
|
+
/** @type {Buffer|null} 对称加密密钥(由 authKey HKDF 派生) */
|
|
166
|
+
#encryptKey = null;
|
|
167
|
+
|
|
168
|
+
/** @type {ReplayGuard} 防重放缓存 */
|
|
169
|
+
#replayGuard;
|
|
170
|
+
|
|
171
|
+
/** @type {number} 时间戳最大允许漂移(毫秒) */
|
|
172
|
+
#maxTimeSkewMs = MAX_TIME_SKEW_MS;
|
|
173
|
+
|
|
174
|
+
/** @type {string|null} slave 侧学习到的 master 节点 ID(来自签名证明) */
|
|
175
|
+
#masterNodeId = null;
|
|
176
|
+
|
|
119
177
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
120
178
|
// 构造函数
|
|
121
179
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
180
|
|
|
123
181
|
/**
|
|
124
182
|
* @param {{
|
|
125
|
-
* role
|
|
126
|
-
* id
|
|
127
|
-
* endpoints?
|
|
128
|
-
* maxPending?
|
|
129
|
-
* authKey?
|
|
130
|
-
* heartbeatInterval?
|
|
183
|
+
* role : "master"|"slave",
|
|
184
|
+
* id : string,
|
|
185
|
+
* endpoints? : { router?: string },
|
|
186
|
+
* maxPending? : number,
|
|
187
|
+
* authKey? : string,
|
|
188
|
+
* heartbeatInterval? : number,
|
|
189
|
+
* heartbeatTimeoutMs? : number,
|
|
190
|
+
* encrypted? : boolean,
|
|
191
|
+
* enablePayloadDigest? : boolean,
|
|
192
|
+
* maxTimeSkewMs? : number,
|
|
193
|
+
* replayWindowMs? : number,
|
|
131
194
|
* }} options
|
|
132
195
|
*/
|
|
133
196
|
constructor({
|
|
134
197
|
role,
|
|
135
198
|
id,
|
|
136
199
|
endpoints = {},
|
|
137
|
-
maxPending =
|
|
200
|
+
maxPending = DEFAULT_MAX_PENDING,
|
|
138
201
|
authKey = "",
|
|
139
202
|
heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
|
|
203
|
+
heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
204
|
+
encrypted = false,
|
|
205
|
+
enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST,
|
|
206
|
+
maxTimeSkewMs = MAX_TIME_SKEW_MS,
|
|
207
|
+
replayWindowMs = REPLAY_WINDOW_MS,
|
|
140
208
|
} = {}) {
|
|
141
209
|
super();
|
|
142
210
|
|
|
@@ -151,12 +219,40 @@ export class ZNL extends EventEmitter {
|
|
|
151
219
|
this.id = String(id);
|
|
152
220
|
this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
|
|
153
221
|
this.authKey = authKey == null ? "" : String(authKey);
|
|
154
|
-
|
|
222
|
+
|
|
223
|
+
this.encrypted = Boolean(encrypted);
|
|
224
|
+
this.#secureEnabled = this.encrypted;
|
|
225
|
+
|
|
226
|
+
if (this.#secureEnabled) {
|
|
227
|
+
if (!this.authKey) {
|
|
228
|
+
throw new Error("启用 encrypted=true 时,必须提供非空 authKey。");
|
|
229
|
+
}
|
|
230
|
+
const { signKey, encryptKey } = deriveKeys(this.authKey);
|
|
231
|
+
this.#signKey = signKey;
|
|
232
|
+
this.#encryptKey = encryptKey;
|
|
233
|
+
}
|
|
155
234
|
|
|
156
235
|
this.#pending = new PendingManager(maxPending);
|
|
157
236
|
this.#sendQueue = new SendQueue();
|
|
158
237
|
this.#heartbeatInterval =
|
|
159
238
|
this.#normalizeHeartbeatInterval(heartbeatInterval);
|
|
239
|
+
|
|
240
|
+
// 是否启用 payload 摘要校验(安全模式用)
|
|
241
|
+
this.#enablePayloadDigest = Boolean(enablePayloadDigest);
|
|
242
|
+
|
|
243
|
+
// 心跳超时:<=0 则由 heartbeatInterval 推导
|
|
244
|
+
this.#heartbeatTimeoutMs = this.#normalizePositiveInt(
|
|
245
|
+
heartbeatTimeoutMs,
|
|
246
|
+
DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
this.#maxTimeSkewMs = this.#normalizePositiveInt(
|
|
250
|
+
maxTimeSkewMs,
|
|
251
|
+
MAX_TIME_SKEW_MS,
|
|
252
|
+
);
|
|
253
|
+
this.#replayGuard = new ReplayGuard({
|
|
254
|
+
windowMs: this.#normalizePositiveInt(replayWindowMs, REPLAY_WINDOW_MS),
|
|
255
|
+
});
|
|
160
256
|
}
|
|
161
257
|
|
|
162
258
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -259,16 +355,23 @@ export class ZNL extends EventEmitter {
|
|
|
259
355
|
const socket = this.#requireSocket("router", "ROUTER");
|
|
260
356
|
if (this.#slaves.size === 0) return;
|
|
261
357
|
|
|
262
|
-
const
|
|
358
|
+
const topicText = String(topic);
|
|
359
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
360
|
+
"publish",
|
|
361
|
+
topicText,
|
|
362
|
+
payload,
|
|
363
|
+
);
|
|
364
|
+
const authProof = this.#secureEnabled
|
|
365
|
+
? this.#createAuthProof("publish", topicText, payloadFrames)
|
|
366
|
+
: "";
|
|
367
|
+
|
|
368
|
+
const frames = buildPublishFrames(topicText, payloadFrames, authProof);
|
|
263
369
|
|
|
264
|
-
// 遍历所有在线 slave,逐一入队发送
|
|
265
|
-
// 使用 #sendQueue 保证 socket 写入安全,不与 RPC 帧交叉
|
|
266
370
|
for (const [slaveId, entry] of this.#slaves) {
|
|
267
371
|
const idFrame = identityToBuffer(entry.identity);
|
|
268
372
|
this.#sendQueue
|
|
269
373
|
.enqueue("router", () => socket.send([idFrame, ...frames]))
|
|
270
374
|
.catch(() => {
|
|
271
|
-
// 发送失败说明 slave 已断线,静默移除并通知外部
|
|
272
375
|
if (this.#slaves.delete(slaveId)) {
|
|
273
376
|
this.emit("slave_disconnected", slaveId);
|
|
274
377
|
}
|
|
@@ -333,7 +436,6 @@ export class ZNL extends EventEmitter {
|
|
|
333
436
|
? this.#startMasterSockets()
|
|
334
437
|
: this.#startSlaveSockets());
|
|
335
438
|
} catch (error) {
|
|
336
|
-
// 启动失败则回滚所有状态
|
|
337
439
|
this.running = false;
|
|
338
440
|
await this.#teardown();
|
|
339
441
|
throw error;
|
|
@@ -348,16 +450,14 @@ export class ZNL extends EventEmitter {
|
|
|
348
450
|
*/
|
|
349
451
|
async #doStop() {
|
|
350
452
|
if (this.role === "slave" && this.#sockets.dealer) {
|
|
351
|
-
// 先停心跳,避免关闭期间继续发送
|
|
352
453
|
clearInterval(this.#heartbeatTimer);
|
|
353
454
|
this.#heartbeatTimer = null;
|
|
354
455
|
|
|
355
|
-
// 优雅下线:在关闭 socket 前先发送注销帧
|
|
356
456
|
await this.#sendQueue
|
|
357
457
|
.enqueue("dealer", () =>
|
|
358
458
|
this.#sockets.dealer.send(buildUnregisterFrames()),
|
|
359
459
|
)
|
|
360
|
-
.catch(() => {});
|
|
460
|
+
.catch(() => {});
|
|
361
461
|
}
|
|
362
462
|
|
|
363
463
|
this.running = false;
|
|
@@ -370,7 +470,6 @@ export class ZNL extends EventEmitter {
|
|
|
370
470
|
* 顺序:停止定时器 → 关闭 socket → 等待读循环退出 → 清空所有注册表
|
|
371
471
|
*/
|
|
372
472
|
async #teardown() {
|
|
373
|
-
// 停止心跳相关定时器
|
|
374
473
|
clearInterval(this.#heartbeatTimer);
|
|
375
474
|
clearInterval(this.#heartbeatCheckTimer);
|
|
376
475
|
this.#heartbeatTimer = null;
|
|
@@ -386,7 +485,9 @@ export class ZNL extends EventEmitter {
|
|
|
386
485
|
this.#readLoops = [];
|
|
387
486
|
this.#sockets = {};
|
|
388
487
|
this.#sendQueue.clear();
|
|
389
|
-
this.#slaves.clear();
|
|
488
|
+
this.#slaves.clear();
|
|
489
|
+
this.#masterNodeId = null;
|
|
490
|
+
this.#replayGuard.clear();
|
|
390
491
|
}
|
|
391
492
|
|
|
392
493
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -410,19 +511,23 @@ export class ZNL extends EventEmitter {
|
|
|
410
511
|
* 连接后立即发送注册帧,再启动心跳定时器
|
|
411
512
|
*/
|
|
412
513
|
async #startSlaveSockets() {
|
|
413
|
-
// routingId 即节点 id,Master 侧通过此字段识别发送方身份
|
|
414
514
|
const dealer = new zmq.Dealer({ routingId: this.id });
|
|
415
515
|
dealer.connect(this.endpoints.router);
|
|
416
516
|
this.#sockets.dealer = dealer;
|
|
417
517
|
|
|
418
518
|
this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
|
|
419
519
|
|
|
420
|
-
//
|
|
520
|
+
// 注册帧:
|
|
521
|
+
// - encrypted=true : 发送签名证明令牌(放在 authKey 字段位)
|
|
522
|
+
// - encrypted=false : 不携带认证信息
|
|
523
|
+
const registerToken = this.#secureEnabled
|
|
524
|
+
? this.#createAuthProof("register", "", [])
|
|
525
|
+
: "";
|
|
526
|
+
|
|
421
527
|
await this.#sendQueue.enqueue("dealer", () =>
|
|
422
|
-
dealer.send(buildRegisterFrames(
|
|
528
|
+
dealer.send(buildRegisterFrames(registerToken)),
|
|
423
529
|
);
|
|
424
530
|
|
|
425
|
-
// 注册成功后启动心跳
|
|
426
531
|
this.#startHeartbeat();
|
|
427
532
|
}
|
|
428
533
|
|
|
@@ -433,7 +538,6 @@ export class ZNL extends EventEmitter {
|
|
|
433
538
|
/**
|
|
434
539
|
* 处理 Master 侧 ROUTER 收到的帧
|
|
435
540
|
* ZMQ ROUTER 帧结构:[identity, ...控制帧]
|
|
436
|
-
* identity 由 ZMQ 自动注入,表示发送方的 routingId
|
|
437
541
|
*/
|
|
438
542
|
async #handleRouterFrames(rawFrames) {
|
|
439
543
|
const [identity, ...bodyFrames] = rawFrames;
|
|
@@ -448,26 +552,49 @@ export class ZNL extends EventEmitter {
|
|
|
448
552
|
...parsed,
|
|
449
553
|
});
|
|
450
554
|
|
|
451
|
-
// ──
|
|
555
|
+
// ── 心跳 ────────────────────────────────────────────────────────────────
|
|
452
556
|
if (parsed.kind === "heartbeat") {
|
|
453
|
-
|
|
454
|
-
|
|
557
|
+
if (this.#secureEnabled) {
|
|
558
|
+
const v = this.#verifyIncomingProof({
|
|
559
|
+
kind: "heartbeat",
|
|
560
|
+
proofToken: parsed.authProof,
|
|
561
|
+
requestId: "",
|
|
562
|
+
payloadFrames: [],
|
|
563
|
+
expectedNodeId: identityText,
|
|
564
|
+
});
|
|
565
|
+
if (!v.ok) {
|
|
566
|
+
this.#emitAuthFailed(event, v.error);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 心跳视为在线确认:必要时自动补注册
|
|
572
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
455
573
|
return;
|
|
456
574
|
}
|
|
457
575
|
|
|
458
|
-
// ──
|
|
576
|
+
// ── 注册 ────────────────────────────────────────────────────────────────
|
|
459
577
|
if (parsed.kind === "register") {
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
578
|
+
if (this.#secureEnabled) {
|
|
579
|
+
const v = this.#verifyIncomingProof({
|
|
580
|
+
kind: "register",
|
|
581
|
+
proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
|
|
582
|
+
requestId: "",
|
|
583
|
+
payloadFrames: [],
|
|
584
|
+
expectedNodeId: identityText,
|
|
585
|
+
});
|
|
586
|
+
if (!v.ok) {
|
|
587
|
+
this.#emitAuthFailed(event, v.error);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
464
590
|
}
|
|
591
|
+
|
|
465
592
|
this.#slaves.set(identityText, { identity, lastSeen: Date.now() });
|
|
466
593
|
this.emit("slave_connected", identityText);
|
|
467
594
|
return;
|
|
468
595
|
}
|
|
469
596
|
|
|
470
|
-
// ──
|
|
597
|
+
// ── 注销 ────────────────────────────────────────────────────────────────
|
|
471
598
|
if (parsed.kind === "unregister") {
|
|
472
599
|
if (this.#slaves.delete(identityText)) {
|
|
473
600
|
this.emit("slave_disconnected", identityText);
|
|
@@ -475,20 +602,54 @@ export class ZNL extends EventEmitter {
|
|
|
475
602
|
return;
|
|
476
603
|
}
|
|
477
604
|
|
|
478
|
-
// ── RPC 请求:slave
|
|
605
|
+
// ── RPC 请求:slave -> master ──────────────────────────────────────────
|
|
479
606
|
if (parsed.kind === "request") {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
607
|
+
let finalFrames = parsed.payloadFrames;
|
|
608
|
+
|
|
609
|
+
if (this.#secureEnabled) {
|
|
610
|
+
const v = this.#verifyIncomingProof({
|
|
611
|
+
kind: "request",
|
|
612
|
+
proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
|
|
613
|
+
requestId: parsed.requestId,
|
|
614
|
+
payloadFrames: parsed.payloadFrames,
|
|
615
|
+
expectedNodeId: identityText,
|
|
616
|
+
});
|
|
617
|
+
if (!v.ok) {
|
|
618
|
+
this.#emitAuthFailed(event, v.error);
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
623
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
624
|
+
|
|
625
|
+
if (this.encrypted) {
|
|
626
|
+
try {
|
|
627
|
+
finalFrames = this.#openPayloadFrames(
|
|
628
|
+
"request",
|
|
629
|
+
parsed.requestId,
|
|
630
|
+
parsed.payloadFrames,
|
|
631
|
+
identityText,
|
|
632
|
+
);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
this.#emitAuthFailed(
|
|
635
|
+
event,
|
|
636
|
+
`请求 payload 解密失败:${error?.message ?? error}`,
|
|
637
|
+
);
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
} else {
|
|
642
|
+
// 明文模式同样补注册
|
|
643
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
484
644
|
}
|
|
485
645
|
|
|
486
|
-
|
|
646
|
+
const finalPayload = payloadFromFrames(finalFrames);
|
|
647
|
+
const requestEvent = { ...event, payload: finalPayload };
|
|
648
|
+
this.emit("request", requestEvent);
|
|
487
649
|
|
|
488
|
-
// 有自动回复处理器时,执行并回复
|
|
489
650
|
if (this.#routerAutoHandler) {
|
|
490
651
|
try {
|
|
491
|
-
const replyPayload = await this.#routerAutoHandler(
|
|
652
|
+
const replyPayload = await this.#routerAutoHandler(requestEvent);
|
|
492
653
|
await this.#replyTo(identity, parsed.requestId, replyPayload);
|
|
493
654
|
} catch (error) {
|
|
494
655
|
this.emit("error", error);
|
|
@@ -497,11 +658,52 @@ export class ZNL extends EventEmitter {
|
|
|
497
658
|
return;
|
|
498
659
|
}
|
|
499
660
|
|
|
500
|
-
// ── RPC 响应:slave
|
|
661
|
+
// ── RPC 响应:slave -> master ──────────────────────────────────────────
|
|
501
662
|
if (parsed.kind === "response") {
|
|
663
|
+
let finalFrames = parsed.payloadFrames;
|
|
664
|
+
|
|
665
|
+
if (this.#secureEnabled) {
|
|
666
|
+
const v = this.#verifyIncomingProof({
|
|
667
|
+
kind: "response",
|
|
668
|
+
proofToken: parsed.authProof,
|
|
669
|
+
requestId: parsed.requestId,
|
|
670
|
+
payloadFrames: parsed.payloadFrames,
|
|
671
|
+
expectedNodeId: identityText,
|
|
672
|
+
});
|
|
673
|
+
if (!v.ok) {
|
|
674
|
+
this.#emitAuthFailed(event, v.error);
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
679
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
680
|
+
|
|
681
|
+
if (this.encrypted) {
|
|
682
|
+
try {
|
|
683
|
+
finalFrames = this.#openPayloadFrames(
|
|
684
|
+
"response",
|
|
685
|
+
parsed.requestId,
|
|
686
|
+
parsed.payloadFrames,
|
|
687
|
+
identityText,
|
|
688
|
+
);
|
|
689
|
+
} catch (error) {
|
|
690
|
+
this.#emitAuthFailed(
|
|
691
|
+
event,
|
|
692
|
+
`响应 payload 解密失败:${error?.message ?? error}`,
|
|
693
|
+
);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
} else {
|
|
698
|
+
// 明文模式同样补注册
|
|
699
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const finalPayload = payloadFromFrames(finalFrames);
|
|
703
|
+
const responseEvent = { ...event, payload: finalPayload };
|
|
502
704
|
const key = this.#pending.key(parsed.requestId, identityText);
|
|
503
|
-
this.#pending.resolve(key,
|
|
504
|
-
this.emit("response",
|
|
705
|
+
this.#pending.resolve(key, responseEvent);
|
|
706
|
+
this.emit("response", responseEvent);
|
|
505
707
|
}
|
|
506
708
|
}
|
|
507
709
|
|
|
@@ -515,14 +717,49 @@ export class ZNL extends EventEmitter {
|
|
|
515
717
|
|
|
516
718
|
const event = this.#buildAndEmit("dealer", frames, { payload, ...parsed });
|
|
517
719
|
|
|
518
|
-
// ── PUB 广播:master
|
|
720
|
+
// ── PUB 广播:master -> slave ──────────────────────────────────────────
|
|
519
721
|
if (parsed.kind === "publish") {
|
|
520
|
-
|
|
722
|
+
let finalFrames = parsed.payloadFrames;
|
|
723
|
+
|
|
724
|
+
if (this.#secureEnabled) {
|
|
725
|
+
const v = this.#verifyIncomingProof({
|
|
726
|
+
kind: "publish",
|
|
727
|
+
proofToken: parsed.authProof,
|
|
728
|
+
requestId: parsed.topic ?? "",
|
|
729
|
+
payloadFrames: parsed.payloadFrames,
|
|
730
|
+
expectedNodeId: this.#masterNodeId,
|
|
731
|
+
});
|
|
732
|
+
if (!v.ok) {
|
|
733
|
+
this.#emitAuthFailed(event, v.error);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// 第一次学习 master 节点 ID,后续锁定
|
|
738
|
+
if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
|
|
739
|
+
|
|
740
|
+
if (this.encrypted) {
|
|
741
|
+
try {
|
|
742
|
+
finalFrames = this.#openPayloadFrames(
|
|
743
|
+
"publish",
|
|
744
|
+
parsed.topic ?? "",
|
|
745
|
+
parsed.payloadFrames,
|
|
746
|
+
this.#masterNodeId,
|
|
747
|
+
);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
this.#emitAuthFailed(
|
|
750
|
+
event,
|
|
751
|
+
`广播 payload 解密失败:${error?.message ?? error}`,
|
|
752
|
+
);
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const finalPayload = payloadFromFrames(finalFrames);
|
|
759
|
+
const pubEvent = { topic: parsed.topic, payload: finalPayload };
|
|
521
760
|
|
|
522
|
-
// 触发统一广播事件(所有 topic 都会触发,方便兜底监听)
|
|
523
761
|
this.emit("publish", pubEvent);
|
|
524
762
|
|
|
525
|
-
// 触发精确 topic 订阅处理器
|
|
526
763
|
const handler = this.#subscriptions.get(parsed.topic);
|
|
527
764
|
if (handler) {
|
|
528
765
|
try {
|
|
@@ -534,13 +771,50 @@ export class ZNL extends EventEmitter {
|
|
|
534
771
|
return;
|
|
535
772
|
}
|
|
536
773
|
|
|
537
|
-
// ── RPC 请求:master
|
|
774
|
+
// ── RPC 请求:master -> slave ──────────────────────────────────────────
|
|
538
775
|
if (parsed.kind === "request") {
|
|
539
|
-
|
|
776
|
+
let finalFrames = parsed.payloadFrames;
|
|
777
|
+
|
|
778
|
+
if (this.#secureEnabled) {
|
|
779
|
+
const v = this.#verifyIncomingProof({
|
|
780
|
+
kind: "request",
|
|
781
|
+
proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
|
|
782
|
+
requestId: parsed.requestId,
|
|
783
|
+
payloadFrames: parsed.payloadFrames,
|
|
784
|
+
expectedNodeId: this.#masterNodeId,
|
|
785
|
+
});
|
|
786
|
+
if (!v.ok) {
|
|
787
|
+
this.#emitAuthFailed(event, v.error);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
|
|
792
|
+
|
|
793
|
+
if (this.encrypted) {
|
|
794
|
+
try {
|
|
795
|
+
finalFrames = this.#openPayloadFrames(
|
|
796
|
+
"request",
|
|
797
|
+
parsed.requestId,
|
|
798
|
+
parsed.payloadFrames,
|
|
799
|
+
this.#masterNodeId,
|
|
800
|
+
);
|
|
801
|
+
} catch (error) {
|
|
802
|
+
this.#emitAuthFailed(
|
|
803
|
+
event,
|
|
804
|
+
`请求 payload 解密失败:${error?.message ?? error}`,
|
|
805
|
+
);
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const finalPayload = payloadFromFrames(finalFrames);
|
|
812
|
+
const requestEvent = { ...event, payload: finalPayload };
|
|
813
|
+
this.emit("request", requestEvent);
|
|
540
814
|
|
|
541
815
|
if (this.#dealerAutoHandler) {
|
|
542
816
|
try {
|
|
543
|
-
const replyPayload = await this.#dealerAutoHandler(
|
|
817
|
+
const replyPayload = await this.#dealerAutoHandler(requestEvent);
|
|
544
818
|
await this.#reply(parsed.requestId, replyPayload);
|
|
545
819
|
} catch (error) {
|
|
546
820
|
this.emit("error", error);
|
|
@@ -549,11 +823,48 @@ export class ZNL extends EventEmitter {
|
|
|
549
823
|
return;
|
|
550
824
|
}
|
|
551
825
|
|
|
552
|
-
// ── RPC 响应:master
|
|
826
|
+
// ── RPC 响应:master -> slave ──────────────────────────────────────────
|
|
553
827
|
if (parsed.kind === "response") {
|
|
828
|
+
let finalFrames = parsed.payloadFrames;
|
|
829
|
+
|
|
830
|
+
if (this.#secureEnabled) {
|
|
831
|
+
const v = this.#verifyIncomingProof({
|
|
832
|
+
kind: "response",
|
|
833
|
+
proofToken: parsed.authProof,
|
|
834
|
+
requestId: parsed.requestId,
|
|
835
|
+
payloadFrames: parsed.payloadFrames,
|
|
836
|
+
expectedNodeId: this.#masterNodeId,
|
|
837
|
+
});
|
|
838
|
+
if (!v.ok) {
|
|
839
|
+
this.#emitAuthFailed(event, v.error);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
|
|
844
|
+
|
|
845
|
+
if (this.encrypted) {
|
|
846
|
+
try {
|
|
847
|
+
finalFrames = this.#openPayloadFrames(
|
|
848
|
+
"response",
|
|
849
|
+
parsed.requestId,
|
|
850
|
+
parsed.payloadFrames,
|
|
851
|
+
this.#masterNodeId,
|
|
852
|
+
);
|
|
853
|
+
} catch (error) {
|
|
854
|
+
this.#emitAuthFailed(
|
|
855
|
+
event,
|
|
856
|
+
`响应 payload 解密失败:${error?.message ?? error}`,
|
|
857
|
+
);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const finalPayload = payloadFromFrames(finalFrames);
|
|
864
|
+
const responseEvent = { ...event, payload: finalPayload };
|
|
554
865
|
const key = this.#pending.key(parsed.requestId);
|
|
555
|
-
this.#pending.resolve(key,
|
|
556
|
-
this.emit("response",
|
|
866
|
+
this.#pending.resolve(key, responseEvent);
|
|
867
|
+
this.emit("response", responseEvent);
|
|
557
868
|
}
|
|
558
869
|
}
|
|
559
870
|
|
|
@@ -563,7 +874,6 @@ export class ZNL extends EventEmitter {
|
|
|
563
874
|
|
|
564
875
|
/**
|
|
565
876
|
* Slave → Master:通过 DEALER socket 发送 RPC 请求,返回响应 Promise
|
|
566
|
-
* 流程:创建 pending → 入队等待发送 → 发送后启动超时计时 → 等待响应 resolve
|
|
567
877
|
*/
|
|
568
878
|
async #request(payload, { timeoutMs } = {}) {
|
|
569
879
|
const socket = this.#requireSocket("dealer", "DEALER");
|
|
@@ -576,13 +886,18 @@ export class ZNL extends EventEmitter {
|
|
|
576
886
|
timeoutMs,
|
|
577
887
|
requestId,
|
|
578
888
|
);
|
|
579
|
-
|
|
889
|
+
|
|
890
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
891
|
+
"request",
|
|
580
892
|
requestId,
|
|
581
|
-
|
|
582
|
-
this.authKey,
|
|
893
|
+
payload,
|
|
583
894
|
);
|
|
895
|
+
const proofOrAuthKey = this.#secureEnabled
|
|
896
|
+
? this.#createAuthProof("request", requestId, payloadFrames)
|
|
897
|
+
: "";
|
|
898
|
+
|
|
899
|
+
const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
|
|
584
900
|
|
|
585
|
-
// 入队:等前一个发送完成后再发;发送完毕后立即启动超时计时
|
|
586
901
|
this.#sendQueue
|
|
587
902
|
.enqueue("dealer", async () => {
|
|
588
903
|
startTimer();
|
|
@@ -602,6 +917,7 @@ export class ZNL extends EventEmitter {
|
|
|
602
917
|
|
|
603
918
|
const identityText = identityToString(identity);
|
|
604
919
|
const idFrame = identityToBuffer(identity);
|
|
920
|
+
|
|
605
921
|
const requestId = randomUUID();
|
|
606
922
|
const key = this.#pending.key(requestId, identityText);
|
|
607
923
|
const { promise, startTimer } = this.#pending.create(
|
|
@@ -610,16 +926,21 @@ export class ZNL extends EventEmitter {
|
|
|
610
926
|
requestId,
|
|
611
927
|
identityText,
|
|
612
928
|
);
|
|
613
|
-
|
|
929
|
+
|
|
930
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
931
|
+
"request",
|
|
614
932
|
requestId,
|
|
615
|
-
|
|
616
|
-
this.authKey,
|
|
933
|
+
payload,
|
|
617
934
|
);
|
|
935
|
+
const proofOrAuthKey = this.#secureEnabled
|
|
936
|
+
? this.#createAuthProof("request", requestId, payloadFrames)
|
|
937
|
+
: "";
|
|
938
|
+
|
|
939
|
+
const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
|
|
618
940
|
|
|
619
941
|
this.#sendQueue
|
|
620
942
|
.enqueue("router", async () => {
|
|
621
943
|
startTimer();
|
|
622
|
-
// ROUTER socket 发送时必须在最前面附上 identity 帧
|
|
623
944
|
await socket.send([idFrame, ...frames]);
|
|
624
945
|
})
|
|
625
946
|
.catch((error) => this.#pending.reject(key, error));
|
|
@@ -638,7 +959,17 @@ export class ZNL extends EventEmitter {
|
|
|
638
959
|
*/
|
|
639
960
|
async #reply(requestId, payload) {
|
|
640
961
|
const socket = this.#requireSocket("dealer", "DEALER");
|
|
641
|
-
|
|
962
|
+
|
|
963
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
964
|
+
"response",
|
|
965
|
+
requestId,
|
|
966
|
+
payload,
|
|
967
|
+
);
|
|
968
|
+
const authProof = this.#secureEnabled
|
|
969
|
+
? this.#createAuthProof("response", requestId, payloadFrames)
|
|
970
|
+
: "";
|
|
971
|
+
|
|
972
|
+
const frames = buildResponseFrames(requestId, payloadFrames, authProof);
|
|
642
973
|
await this.#sendQueue.enqueue("dealer", () => socket.send(frames));
|
|
643
974
|
}
|
|
644
975
|
|
|
@@ -651,7 +982,17 @@ export class ZNL extends EventEmitter {
|
|
|
651
982
|
async #replyTo(identity, requestId, payload) {
|
|
652
983
|
const socket = this.#requireSocket("router", "ROUTER");
|
|
653
984
|
const idFrame = identityToBuffer(identity);
|
|
654
|
-
|
|
985
|
+
|
|
986
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
987
|
+
"response",
|
|
988
|
+
requestId,
|
|
989
|
+
payload,
|
|
990
|
+
);
|
|
991
|
+
const authProof = this.#secureEnabled
|
|
992
|
+
? this.#createAuthProof("response", requestId, payloadFrames)
|
|
993
|
+
: "";
|
|
994
|
+
|
|
995
|
+
const frames = buildResponseFrames(requestId, payloadFrames, authProof);
|
|
655
996
|
await this.#sendQueue.enqueue("router", () =>
|
|
656
997
|
socket.send([idFrame, ...frames]),
|
|
657
998
|
);
|
|
@@ -663,30 +1004,39 @@ export class ZNL extends EventEmitter {
|
|
|
663
1004
|
|
|
664
1005
|
/**
|
|
665
1006
|
* 启动 slave 侧心跳发送定时器
|
|
666
|
-
* 每隔 #heartbeatInterval ms 向 master 发送一帧心跳,证明自己还活着
|
|
667
1007
|
*/
|
|
668
1008
|
#startHeartbeat() {
|
|
669
1009
|
if (this.#heartbeatInterval <= 0) return;
|
|
670
1010
|
|
|
671
1011
|
this.#heartbeatTimer = setInterval(() => {
|
|
672
1012
|
if (!this.running || !this.#sockets.dealer) return;
|
|
1013
|
+
|
|
1014
|
+
const proof = this.#secureEnabled
|
|
1015
|
+
? this.#createAuthProof("heartbeat", "", [])
|
|
1016
|
+
: "";
|
|
1017
|
+
|
|
1018
|
+
// plain 模式仍保持历史帧结构 [CONTROL_PREFIX, CONTROL_HEARTBEAT]
|
|
1019
|
+
const frames = this.#secureEnabled
|
|
1020
|
+
? buildHeartbeatFrames(proof)
|
|
1021
|
+
: [CONTROL_PREFIX, CONTROL_HEARTBEAT];
|
|
1022
|
+
|
|
673
1023
|
this.#sendQueue
|
|
674
|
-
.enqueue("dealer", () =>
|
|
675
|
-
|
|
676
|
-
)
|
|
677
|
-
.catch(() => {}); // 发送失败静默处理,不影响业务
|
|
1024
|
+
.enqueue("dealer", () => this.#sockets.dealer.send(frames))
|
|
1025
|
+
.catch(() => {});
|
|
678
1026
|
}, this.#heartbeatInterval);
|
|
679
1027
|
}
|
|
680
1028
|
|
|
681
1029
|
/**
|
|
682
1030
|
* 启动 master 侧死节点扫描定时器
|
|
683
|
-
* 每隔 #heartbeatInterval ms 扫描一次,将超过 3 个周期未活跃的 slave 视为崩溃并移除
|
|
684
1031
|
*/
|
|
685
1032
|
#startHeartbeatCheck() {
|
|
686
1033
|
if (this.#heartbeatInterval <= 0) return;
|
|
687
1034
|
|
|
688
|
-
//
|
|
689
|
-
const timeout =
|
|
1035
|
+
// 心跳超时:优先使用配置值,<=0 则回退到 interval × 3
|
|
1036
|
+
const timeout =
|
|
1037
|
+
this.#heartbeatTimeoutMs > 0
|
|
1038
|
+
? this.#heartbeatTimeoutMs
|
|
1039
|
+
: this.#heartbeatInterval * 3;
|
|
690
1040
|
|
|
691
1041
|
this.#heartbeatCheckTimer = setInterval(() => {
|
|
692
1042
|
const now = Date.now();
|
|
@@ -699,14 +1049,228 @@ export class ZNL extends EventEmitter {
|
|
|
699
1049
|
}, this.#heartbeatInterval);
|
|
700
1050
|
}
|
|
701
1051
|
|
|
1052
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1053
|
+
// 安全辅助(签名/防重放/加密)
|
|
1054
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* 生成签名证明令牌
|
|
1058
|
+
*
|
|
1059
|
+
* @param {"register"|"heartbeat"|"request"|"response"|"publish"} kind
|
|
1060
|
+
* @param {string} requestId
|
|
1061
|
+
* @param {Buffer[]} payloadFrames
|
|
1062
|
+
* @returns {string}
|
|
1063
|
+
*/
|
|
1064
|
+
#createAuthProof(kind, requestId, payloadFrames) {
|
|
1065
|
+
if (!this.#secureEnabled || !this.#signKey) return "";
|
|
1066
|
+
|
|
1067
|
+
const envelope = {
|
|
1068
|
+
kind: String(kind),
|
|
1069
|
+
nodeId: this.id,
|
|
1070
|
+
requestId: String(requestId ?? ""),
|
|
1071
|
+
timestamp: nowMs(),
|
|
1072
|
+
nonce: generateNonce(),
|
|
1073
|
+
// 可选 payload 摘要:关闭时写空串以提升性能
|
|
1074
|
+
payloadDigest: this.#enablePayloadDigest
|
|
1075
|
+
? digestFrames(payloadFrames)
|
|
1076
|
+
: "",
|
|
1077
|
+
};
|
|
1078
|
+
|
|
1079
|
+
// 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
|
|
1080
|
+
canonicalSignInput(envelope);
|
|
1081
|
+
|
|
1082
|
+
return encodeAuthProofToken(this.#signKey, envelope);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* 校验入站签名证明 + 防重放 + 摘要一致性
|
|
1087
|
+
*
|
|
1088
|
+
* @param {{
|
|
1089
|
+
* kind: "register"|"heartbeat"|"request"|"response"|"publish",
|
|
1090
|
+
* proofToken: string|null,
|
|
1091
|
+
* requestId: string|null,
|
|
1092
|
+
* payloadFrames: Buffer[],
|
|
1093
|
+
* expectedNodeId?: string|null,
|
|
1094
|
+
* }} input
|
|
1095
|
+
* @returns {{ ok: true, envelope: any } | { ok: false, error: string }}
|
|
1096
|
+
*/
|
|
1097
|
+
#verifyIncomingProof({
|
|
1098
|
+
kind,
|
|
1099
|
+
proofToken,
|
|
1100
|
+
requestId,
|
|
1101
|
+
payloadFrames,
|
|
1102
|
+
expectedNodeId = null,
|
|
1103
|
+
}) {
|
|
1104
|
+
if (!this.#secureEnabled) return { ok: true, envelope: null };
|
|
1105
|
+
if (!this.#signKey) {
|
|
1106
|
+
return { ok: false, error: "签名密钥未初始化。" };
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
if (!proofToken) {
|
|
1110
|
+
return { ok: false, error: "缺少认证证明(proofToken)。" };
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const decoded = decodeAuthProofToken(this.#signKey, proofToken, {
|
|
1114
|
+
maxSkewMs: this.#maxTimeSkewMs,
|
|
1115
|
+
now: Date.now(),
|
|
1116
|
+
});
|
|
1117
|
+
if (!decoded.ok) {
|
|
1118
|
+
return { ok: false, error: decoded.error || "认证证明解析失败。" };
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const envelope = decoded.envelope;
|
|
1122
|
+
|
|
1123
|
+
if (envelope.kind !== String(kind)) {
|
|
1124
|
+
return {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
error: `证明 kind 不匹配:expect=${kind}, got=${envelope.kind}`,
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (String(envelope.requestId ?? "") !== String(requestId ?? "")) {
|
|
1131
|
+
return {
|
|
1132
|
+
ok: false,
|
|
1133
|
+
error: `证明 requestId 不匹配:expect=${requestId ?? ""}, got=${envelope.requestId ?? ""}`,
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (expectedNodeId && String(envelope.nodeId) !== String(expectedNodeId)) {
|
|
1138
|
+
return {
|
|
1139
|
+
ok: false,
|
|
1140
|
+
error: `证明 nodeId 不匹配:expect=${expectedNodeId}, got=${envelope.nodeId}`,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
if (this.#enablePayloadDigest) {
|
|
1145
|
+
const currentDigest = digestFrames(payloadFrames);
|
|
1146
|
+
if (String(envelope.payloadDigest) !== currentDigest) {
|
|
1147
|
+
return { ok: false, error: "payload 摘要不一致,疑似篡改。" };
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// 重放 key 加入 requestId,降低 nonce 碰撞误杀风险
|
|
1152
|
+
const replayKey = `${envelope.kind}|${envelope.nodeId}|${envelope.requestId}|${envelope.nonce}`;
|
|
1153
|
+
if (this.#replayGuard.seenOrAdd(replayKey)) {
|
|
1154
|
+
return { ok: false, error: "检测到重放请求(nonce 重复)。" };
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
return { ok: true, envelope };
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* 出站 payload 处理:
|
|
1162
|
+
* - plain/auth : 维持原有 normalizeFrames
|
|
1163
|
+
* - encrypted : 透明加密为安全信封
|
|
1164
|
+
*
|
|
1165
|
+
* @param {"request"|"response"|"publish"} kind
|
|
1166
|
+
* @param {string} requestId
|
|
1167
|
+
* @param {*} payload
|
|
1168
|
+
* @returns {Buffer[]}
|
|
1169
|
+
*/
|
|
1170
|
+
#sealPayloadFrames(kind, requestId, payload) {
|
|
1171
|
+
if (!this.encrypted) {
|
|
1172
|
+
// 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
|
|
1173
|
+
return normalizeFrames(payload);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
if (!this.#encryptKey) {
|
|
1177
|
+
throw new Error("加密密钥未初始化。");
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
const rawFrames = toFrameBuffers(payload);
|
|
1181
|
+
const aad = Buffer.from(
|
|
1182
|
+
`znl-aad-v1|${String(kind)}|${String(this.id)}|${String(requestId ?? "")}`,
|
|
1183
|
+
"utf8",
|
|
1184
|
+
);
|
|
1185
|
+
|
|
1186
|
+
const { iv, ciphertext, tag } = encryptFrames(
|
|
1187
|
+
this.#encryptKey,
|
|
1188
|
+
rawFrames,
|
|
1189
|
+
aad,
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
// 用统一信封包装:version + iv + tag + ciphertext
|
|
1193
|
+
return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/**
|
|
1197
|
+
* 入站 payload 解封:
|
|
1198
|
+
* - encrypted=true 时要求收到加密信封
|
|
1199
|
+
* - 解密后恢复为 Buffer[],再由 payloadFromFrames 还原
|
|
1200
|
+
*
|
|
1201
|
+
* @param {"request"|"response"|"publish"} kind
|
|
1202
|
+
* @param {string} requestId
|
|
1203
|
+
* @param {Buffer[]} payloadFrames
|
|
1204
|
+
* @param {string} senderNodeId
|
|
1205
|
+
* @returns {Buffer[]}
|
|
1206
|
+
*/
|
|
1207
|
+
#openPayloadFrames(kind, requestId, payloadFrames, senderNodeId) {
|
|
1208
|
+
if (!this.encrypted) return payloadFrames;
|
|
1209
|
+
if (!this.#encryptKey) throw new Error("加密密钥未初始化。");
|
|
1210
|
+
|
|
1211
|
+
if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
|
|
1212
|
+
throw new Error("加密信封格式非法:期望 4 帧。");
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
const [version, iv, tag, ciphertext] = payloadFrames;
|
|
1216
|
+
if (String(version?.toString?.() ?? "") !== SECURITY_ENVELOPE_VERSION) {
|
|
1217
|
+
throw new Error("加密信封版本不匹配。");
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
const aad = Buffer.from(
|
|
1221
|
+
`znl-aad-v1|${String(kind)}|${String(senderNodeId)}|${String(requestId ?? "")}`,
|
|
1222
|
+
"utf8",
|
|
1223
|
+
);
|
|
1224
|
+
|
|
1225
|
+
return decryptFrames(this.#encryptKey, iv, ciphertext, tag, aad);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* 触发统一 auth_failed 事件
|
|
1230
|
+
* @param {object} baseEvent
|
|
1231
|
+
* @param {string} reason
|
|
1232
|
+
*/
|
|
1233
|
+
#emitAuthFailed(baseEvent, reason) {
|
|
1234
|
+
this.emit("auth_failed", {
|
|
1235
|
+
...baseEvent,
|
|
1236
|
+
reason: String(reason ?? "认证失败"),
|
|
1237
|
+
encrypted: this.encrypted,
|
|
1238
|
+
});
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
/**
|
|
1242
|
+
* master 侧确保某个 slave 已登记在线
|
|
1243
|
+
* - 仅在认证通过后调用
|
|
1244
|
+
* - 支持心跳/请求/响应等路径的自动补注册
|
|
1245
|
+
*
|
|
1246
|
+
* @param {string} identityText
|
|
1247
|
+
* @param {Buffer|string|Uint8Array} identity
|
|
1248
|
+
* @param {{ touch?: boolean }} [options]
|
|
1249
|
+
*/
|
|
1250
|
+
#ensureSlaveOnline(identityText, identity, { touch = true } = {}) {
|
|
1251
|
+
const id = String(identityText ?? "");
|
|
1252
|
+
if (!id) return;
|
|
1253
|
+
|
|
1254
|
+
const now = Date.now();
|
|
1255
|
+
const entry = this.#slaves.get(id);
|
|
1256
|
+
if (entry) {
|
|
1257
|
+
if (touch) entry.lastSeen = now;
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
this.#slaves.set(id, {
|
|
1262
|
+
identity: identityToBuffer(identity),
|
|
1263
|
+
lastSeen: now,
|
|
1264
|
+
});
|
|
1265
|
+
this.emit("slave_connected", id);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
702
1268
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
703
1269
|
// 工具方法
|
|
704
1270
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
705
1271
|
|
|
706
1272
|
/**
|
|
707
1273
|
* 启动 socket 的异步读取循环(for-await-of)
|
|
708
|
-
* 循环在 socket 被关闭后自然退出,Promise 保存到 #readLoops 供 teardown 等待
|
|
709
|
-
*
|
|
710
1274
|
* @param {import("zeromq").Socket} socket
|
|
711
1275
|
* @param {(frames: Array) => Promise<void>} handler
|
|
712
1276
|
*/
|
|
@@ -715,7 +1279,6 @@ export class ZNL extends EventEmitter {
|
|
|
715
1279
|
try {
|
|
716
1280
|
for await (const rawFrames of socket) {
|
|
717
1281
|
if (!this.running) return;
|
|
718
|
-
// zeromq v6 单帧时返回 Buffer,多帧时返回数组,统一转为数组处理
|
|
719
1282
|
const frames = Array.isArray(rawFrames) ? rawFrames : [rawFrames];
|
|
720
1283
|
try {
|
|
721
1284
|
await handler(frames);
|
|
@@ -724,7 +1287,6 @@ export class ZNL extends EventEmitter {
|
|
|
724
1287
|
}
|
|
725
1288
|
}
|
|
726
1289
|
} catch (error) {
|
|
727
|
-
// socket 关闭时 for-await 会抛出,仅在运行中时才视为错误
|
|
728
1290
|
if (this.running) this.emit("error", error);
|
|
729
1291
|
}
|
|
730
1292
|
})();
|
|
@@ -737,7 +1299,7 @@ export class ZNL extends EventEmitter {
|
|
|
737
1299
|
* @param {string} channel
|
|
738
1300
|
* @param {Array} frames
|
|
739
1301
|
* @param {object} extra
|
|
740
|
-
* @returns {object}
|
|
1302
|
+
* @returns {object}
|
|
741
1303
|
*/
|
|
742
1304
|
#buildAndEmit(channel, frames, extra = {}) {
|
|
743
1305
|
const event = { channel, frames, ...extra };
|
|
@@ -747,7 +1309,7 @@ export class ZNL extends EventEmitter {
|
|
|
747
1309
|
}
|
|
748
1310
|
|
|
749
1311
|
/**
|
|
750
|
-
* 获取指定 socket
|
|
1312
|
+
* 获取指定 socket,不存在时抛出明确错误
|
|
751
1313
|
* @param {string} name
|
|
752
1314
|
* @param {string} displayName
|
|
753
1315
|
* @returns {import("zeromq").Socket}
|
|
@@ -770,4 +1332,16 @@ export class ZNL extends EventEmitter {
|
|
|
770
1332
|
if (Number.isFinite(v) && v >= 0) return Math.floor(v);
|
|
771
1333
|
return DEFAULT_HEARTBEAT_INTERVAL;
|
|
772
1334
|
}
|
|
1335
|
+
|
|
1336
|
+
/**
|
|
1337
|
+
* 规范化正整数(<=0 时回退默认值)
|
|
1338
|
+
* @param {*} n
|
|
1339
|
+
* @param {number} fallback
|
|
1340
|
+
* @returns {number}
|
|
1341
|
+
*/
|
|
1342
|
+
#normalizePositiveInt(n, fallback) {
|
|
1343
|
+
const v = Number(n);
|
|
1344
|
+
if (Number.isFinite(v) && v > 0) return Math.floor(v);
|
|
1345
|
+
return fallback;
|
|
1346
|
+
}
|
|
773
1347
|
}
|