@lyrify/znl 0.5.2 → 0.6.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 +405 -86
- package/package.json +1 -1
- package/src/ZNL.js +607 -76
- package/src/constants.js +3 -0
- package/src/protocol.js +47 -12
package/src/ZNL.js
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
buildRegisterFrames,
|
|
50
50
|
buildUnregisterFrames,
|
|
51
51
|
buildHeartbeatFrames,
|
|
52
|
+
buildHeartbeatAckFrames,
|
|
52
53
|
buildRequestFrames,
|
|
53
54
|
buildResponseFrames,
|
|
54
55
|
buildPublishFrames,
|
|
@@ -127,13 +128,19 @@ export class ZNL extends EventEmitter {
|
|
|
127
128
|
/** @type {boolean} 是否启用 payload 摘要校验(安全模式用) */
|
|
128
129
|
#enablePayloadDigest = DEFAULT_ENABLE_PAYLOAD_DIGEST;
|
|
129
130
|
|
|
131
|
+
/** @type {Map<string, string>} master 侧 authKey 配置表(按 slaveId) */
|
|
132
|
+
#authKeyMap = new Map();
|
|
133
|
+
|
|
134
|
+
/** @type {Map<string, { signKey: Buffer, encryptKey: Buffer }>} master 侧派生密钥缓存 */
|
|
135
|
+
#slaveKeyCache = new Map();
|
|
136
|
+
|
|
130
137
|
// ─── PUB/SUB 状态 ────────────────────────────────────────────────────────
|
|
131
138
|
|
|
132
139
|
/**
|
|
133
140
|
* master 侧:已注册的在线 slave 表
|
|
134
141
|
* key = slaveId(字符串)
|
|
135
|
-
* value = { identity: Buffer, lastSeen: number }
|
|
136
|
-
* @type {Map<string, { identity: Buffer, lastSeen: number }>}
|
|
142
|
+
* value = { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }
|
|
143
|
+
* @type {Map<string, { identity: Buffer, lastSeen: number, signKey: Buffer|null, encryptKey: Buffer|null }>}
|
|
137
144
|
*/
|
|
138
145
|
#slaves = new Map();
|
|
139
146
|
|
|
@@ -148,9 +155,24 @@ export class ZNL extends EventEmitter {
|
|
|
148
155
|
/** 心跳间隔(ms),0 表示禁用;slave/master 共用同一配置 */
|
|
149
156
|
#heartbeatInterval;
|
|
150
157
|
|
|
151
|
-
/** slave
|
|
158
|
+
/** slave 侧单次心跳调度定时器句柄 */
|
|
152
159
|
#heartbeatTimer = null;
|
|
153
160
|
|
|
161
|
+
/** slave 侧等待心跳应答的超时定时器句柄 */
|
|
162
|
+
#heartbeatAckTimer = null;
|
|
163
|
+
|
|
164
|
+
/** slave 侧当前是否存在未确认的心跳 */
|
|
165
|
+
#heartbeatWaitingAck = false;
|
|
166
|
+
|
|
167
|
+
/** slave 侧最近一次确认主节点在线的时间戳 */
|
|
168
|
+
#lastMasterSeenAt = 0;
|
|
169
|
+
|
|
170
|
+
/** slave 侧缓存的主节点在线状态 */
|
|
171
|
+
#masterOnline = false;
|
|
172
|
+
|
|
173
|
+
/** slave 侧正在进行的 Dealer 重连 Promise(防止并发重连) */
|
|
174
|
+
#dealerReconnectPromise = null;
|
|
175
|
+
|
|
154
176
|
/** master 侧扫描死节点的定时器句柄 */
|
|
155
177
|
#heartbeatCheckTimer = null;
|
|
156
178
|
|
|
@@ -185,6 +207,7 @@ export class ZNL extends EventEmitter {
|
|
|
185
207
|
* endpoints? : { router?: string },
|
|
186
208
|
* maxPending? : number,
|
|
187
209
|
* authKey? : string,
|
|
210
|
+
* authKeyMap? : Record<string, string>,
|
|
188
211
|
* heartbeatInterval? : number,
|
|
189
212
|
* heartbeatTimeoutMs? : number,
|
|
190
213
|
* encrypted? : boolean,
|
|
@@ -199,6 +222,7 @@ export class ZNL extends EventEmitter {
|
|
|
199
222
|
endpoints = {},
|
|
200
223
|
maxPending = DEFAULT_MAX_PENDING,
|
|
201
224
|
authKey = "",
|
|
225
|
+
authKeyMap = null,
|
|
202
226
|
heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL,
|
|
203
227
|
heartbeatTimeoutMs = DEFAULT_HEARTBEAT_TIMEOUT_MS,
|
|
204
228
|
encrypted = false,
|
|
@@ -220,16 +244,35 @@ export class ZNL extends EventEmitter {
|
|
|
220
244
|
this.endpoints = { ...DEFAULT_ENDPOINTS, ...endpoints };
|
|
221
245
|
this.authKey = authKey == null ? "" : String(authKey);
|
|
222
246
|
|
|
247
|
+
if (this.role === "master") {
|
|
248
|
+
if (authKeyMap != null) {
|
|
249
|
+
if (typeof authKeyMap !== "object") {
|
|
250
|
+
throw new TypeError(
|
|
251
|
+
"`authKeyMap` 必须是对象(slaveId -> authKey)。",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
for (const [slaveId, key] of Object.entries(authKeyMap)) {
|
|
255
|
+
if (key == null) continue;
|
|
256
|
+
this.#authKeyMap.set(String(slaveId), String(key));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
223
261
|
this.encrypted = Boolean(encrypted);
|
|
224
262
|
this.#secureEnabled = this.encrypted;
|
|
225
263
|
|
|
226
264
|
if (this.#secureEnabled) {
|
|
227
|
-
|
|
228
|
-
|
|
265
|
+
const hasAuthMap = this.role === "master" && authKeyMap != null;
|
|
266
|
+
if (!this.authKey && !hasAuthMap) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"启用 encrypted=true 时,必须提供非空 authKey 或 authKeyMap。",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
if (this.authKey) {
|
|
272
|
+
const { signKey, encryptKey } = deriveKeys(this.authKey);
|
|
273
|
+
this.#signKey = signKey;
|
|
274
|
+
this.#encryptKey = encryptKey;
|
|
229
275
|
}
|
|
230
|
-
const { signKey, encryptKey } = deriveKeys(this.authKey);
|
|
231
|
-
this.#signKey = signKey;
|
|
232
|
-
this.#encryptKey = encryptKey;
|
|
233
276
|
}
|
|
234
277
|
|
|
235
278
|
this.#pending = new PendingManager(maxPending);
|
|
@@ -356,19 +399,34 @@ export class ZNL extends EventEmitter {
|
|
|
356
399
|
if (this.#slaves.size === 0) return;
|
|
357
400
|
|
|
358
401
|
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);
|
|
369
402
|
|
|
370
403
|
for (const [slaveId, entry] of this.#slaves) {
|
|
404
|
+
const keys = this.#secureEnabled ? this.#resolveSlaveKeys(slaveId) : null;
|
|
405
|
+
if (this.#secureEnabled && !keys) {
|
|
406
|
+
if (this.#slaves.delete(slaveId)) {
|
|
407
|
+
this.emit("slave_disconnected", slaveId);
|
|
408
|
+
}
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const payloadFrames = this.#sealPayloadFrames(
|
|
413
|
+
"publish",
|
|
414
|
+
topicText,
|
|
415
|
+
payload,
|
|
416
|
+
keys?.encryptKey ?? null,
|
|
417
|
+
);
|
|
418
|
+
const authProof = this.#secureEnabled
|
|
419
|
+
? this.#createAuthProof(
|
|
420
|
+
"publish",
|
|
421
|
+
topicText,
|
|
422
|
+
payloadFrames,
|
|
423
|
+
keys.signKey,
|
|
424
|
+
)
|
|
425
|
+
: "";
|
|
426
|
+
|
|
427
|
+
const frames = buildPublishFrames(topicText, payloadFrames, authProof);
|
|
371
428
|
const idFrame = identityToBuffer(entry.identity);
|
|
429
|
+
|
|
372
430
|
this.#sendQueue
|
|
373
431
|
.enqueue("router", () => socket.send([idFrame, ...frames]))
|
|
374
432
|
.catch(() => {
|
|
@@ -424,6 +482,84 @@ export class ZNL extends EventEmitter {
|
|
|
424
482
|
return [...this.#slaves.keys()];
|
|
425
483
|
}
|
|
426
484
|
|
|
485
|
+
/**
|
|
486
|
+
* 【Slave 侧】当前已确认的主节点在线状态
|
|
487
|
+
* - 仅在 slave 角色下有意义
|
|
488
|
+
* - 收到合法 heartbeat_ack / request / response / publish 时置为 true
|
|
489
|
+
* - 心跳应答超时、重连、stop 时置为 false
|
|
490
|
+
*/
|
|
491
|
+
get masterOnline() {
|
|
492
|
+
return this.role === "slave" ? this.#masterOnline : false;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* 【Slave 侧】查询当前主节点是否在线
|
|
497
|
+
* 说明:
|
|
498
|
+
* - 这是对外公开的轻量 API,适合业务层主动轮询
|
|
499
|
+
* - 返回值基于最近一次链路确认结果,而非一次实时网络探测
|
|
500
|
+
* @returns {boolean}
|
|
501
|
+
*/
|
|
502
|
+
isMasterOnline() {
|
|
503
|
+
return this.masterOnline;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 【Master 侧】动态添加/更新某个 slave 的 authKey(立即生效)
|
|
508
|
+
* @param {string} slaveId
|
|
509
|
+
* @param {string} authKey
|
|
510
|
+
* @returns {this}
|
|
511
|
+
*/
|
|
512
|
+
addAuthKey(slaveId, authKey) {
|
|
513
|
+
if (this.role !== "master") {
|
|
514
|
+
throw new Error("addAuthKey() 只能在 master 侧调用。");
|
|
515
|
+
}
|
|
516
|
+
const id = String(slaveId ?? "");
|
|
517
|
+
if (!id) {
|
|
518
|
+
throw new Error("slaveId 不能为空。");
|
|
519
|
+
}
|
|
520
|
+
const key = authKey == null ? "" : String(authKey);
|
|
521
|
+
if (!key) {
|
|
522
|
+
throw new Error("authKey 不能为空。");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
this.#authKeyMap.set(id, key);
|
|
526
|
+
|
|
527
|
+
if (this.#secureEnabled) {
|
|
528
|
+
const derived = deriveKeys(key);
|
|
529
|
+
this.#slaveKeyCache.set(id, derived);
|
|
530
|
+
const entry = this.#slaves.get(id);
|
|
531
|
+
if (entry) {
|
|
532
|
+
entry.signKey = derived.signKey;
|
|
533
|
+
entry.encryptKey = derived.encryptKey;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* 【Master 侧】移除某个 slave 的 authKey(立即生效)
|
|
541
|
+
* @param {string} slaveId
|
|
542
|
+
* @returns {this}
|
|
543
|
+
*/
|
|
544
|
+
removeAuthKey(slaveId) {
|
|
545
|
+
if (this.role !== "master") {
|
|
546
|
+
throw new Error("removeAuthKey() 只能在 master 侧调用。");
|
|
547
|
+
}
|
|
548
|
+
const id = String(slaveId ?? "");
|
|
549
|
+
if (!id) {
|
|
550
|
+
throw new Error("slaveId 不能为空。");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
this.#authKeyMap.delete(id);
|
|
554
|
+
this.#slaveKeyCache.delete(id);
|
|
555
|
+
|
|
556
|
+
if (this.#slaves.has(id)) {
|
|
557
|
+
this.#slaves.delete(id);
|
|
558
|
+
this.emit("slave_disconnected", id);
|
|
559
|
+
}
|
|
560
|
+
return this;
|
|
561
|
+
}
|
|
562
|
+
|
|
427
563
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
428
564
|
// 生命周期内部实现
|
|
429
565
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -450,8 +586,13 @@ export class ZNL extends EventEmitter {
|
|
|
450
586
|
*/
|
|
451
587
|
async #doStop() {
|
|
452
588
|
if (this.role === "slave" && this.#sockets.dealer) {
|
|
453
|
-
|
|
589
|
+
clearTimeout(this.#heartbeatTimer);
|
|
590
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
454
591
|
this.#heartbeatTimer = null;
|
|
592
|
+
this.#heartbeatAckTimer = null;
|
|
593
|
+
this.#heartbeatWaitingAck = false;
|
|
594
|
+
this.#masterOnline = false;
|
|
595
|
+
this.#lastMasterSeenAt = 0;
|
|
455
596
|
|
|
456
597
|
await this.#sendQueue
|
|
457
598
|
.enqueue("dealer", () =>
|
|
@@ -470,9 +611,11 @@ export class ZNL extends EventEmitter {
|
|
|
470
611
|
* 顺序:停止定时器 → 关闭 socket → 等待读循环退出 → 清空所有注册表
|
|
471
612
|
*/
|
|
472
613
|
async #teardown() {
|
|
473
|
-
|
|
614
|
+
clearTimeout(this.#heartbeatTimer);
|
|
615
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
474
616
|
clearInterval(this.#heartbeatCheckTimer);
|
|
475
617
|
this.#heartbeatTimer = null;
|
|
618
|
+
this.#heartbeatAckTimer = null;
|
|
476
619
|
this.#heartbeatCheckTimer = null;
|
|
477
620
|
|
|
478
621
|
for (const socket of Object.values(this.#sockets)) {
|
|
@@ -486,7 +629,12 @@ export class ZNL extends EventEmitter {
|
|
|
486
629
|
this.#sockets = {};
|
|
487
630
|
this.#sendQueue.clear();
|
|
488
631
|
this.#slaves.clear();
|
|
632
|
+
this.#slaveKeyCache.clear();
|
|
489
633
|
this.#masterNodeId = null;
|
|
634
|
+
this.#masterOnline = false;
|
|
635
|
+
this.#lastMasterSeenAt = 0;
|
|
636
|
+
this.#heartbeatWaitingAck = false;
|
|
637
|
+
this.#dealerReconnectPromise = null;
|
|
490
638
|
this.#replayGuard.clear();
|
|
491
639
|
}
|
|
492
640
|
|
|
@@ -498,7 +646,7 @@ export class ZNL extends EventEmitter {
|
|
|
498
646
|
* 初始化 Master 侧 ROUTER socket(bind 模式),并启动读循环和死节点扫描定时器
|
|
499
647
|
*/
|
|
500
648
|
async #startMasterSockets() {
|
|
501
|
-
const router = new zmq.Router();
|
|
649
|
+
const router = new zmq.Router({ handover: true });
|
|
502
650
|
await router.bind(this.endpoints.router);
|
|
503
651
|
this.#sockets.router = router;
|
|
504
652
|
|
|
@@ -511,23 +659,17 @@ export class ZNL extends EventEmitter {
|
|
|
511
659
|
* 连接后立即发送注册帧,再启动心跳定时器
|
|
512
660
|
*/
|
|
513
661
|
async #startSlaveSockets() {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
this.#
|
|
517
|
-
|
|
518
|
-
this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
|
|
662
|
+
this.#masterOnline = false;
|
|
663
|
+
this.#lastMasterSeenAt = 0;
|
|
664
|
+
this.#heartbeatWaitingAck = false;
|
|
519
665
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
// - encrypted=false : 不携带认证信息
|
|
523
|
-
const registerToken = this.#secureEnabled
|
|
524
|
-
? this.#createAuthProof("register", "", [])
|
|
525
|
-
: "";
|
|
526
|
-
|
|
527
|
-
await this.#sendQueue.enqueue("dealer", () =>
|
|
528
|
-
dealer.send(buildRegisterFrames(registerToken)),
|
|
529
|
-
);
|
|
666
|
+
const dealer = this.#createDealerSocket();
|
|
667
|
+
this.#sockets.dealer = dealer;
|
|
530
668
|
|
|
669
|
+
// 注册帧改为“尽力发送”:
|
|
670
|
+
// - master 尚未上线时无需阻塞启动流程
|
|
671
|
+
// - 后续 heartbeat_ack 成功后仍可自然恢复在线状态
|
|
672
|
+
this.#trySendRegister();
|
|
531
673
|
this.#startHeartbeat();
|
|
532
674
|
}
|
|
533
675
|
|
|
@@ -554,42 +696,92 @@ export class ZNL extends EventEmitter {
|
|
|
554
696
|
|
|
555
697
|
// ── 心跳 ────────────────────────────────────────────────────────────────
|
|
556
698
|
if (parsed.kind === "heartbeat") {
|
|
699
|
+
let ackFrames = buildHeartbeatAckFrames();
|
|
700
|
+
|
|
557
701
|
if (this.#secureEnabled) {
|
|
702
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
703
|
+
if (!keys) {
|
|
704
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
705
|
+
if (this.#slaves.delete(identityText)) {
|
|
706
|
+
this.emit("slave_disconnected", identityText);
|
|
707
|
+
}
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
558
711
|
const v = this.#verifyIncomingProof({
|
|
559
712
|
kind: "heartbeat",
|
|
560
713
|
proofToken: parsed.authProof,
|
|
561
714
|
requestId: "",
|
|
562
715
|
payloadFrames: [],
|
|
563
716
|
expectedNodeId: identityText,
|
|
717
|
+
signKey: keys.signKey,
|
|
564
718
|
});
|
|
565
719
|
if (!v.ok) {
|
|
566
720
|
this.#emitAuthFailed(event, v.error);
|
|
567
721
|
return;
|
|
568
722
|
}
|
|
723
|
+
|
|
724
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
725
|
+
touch: true,
|
|
726
|
+
signKey: keys.signKey,
|
|
727
|
+
encryptKey: keys.encryptKey,
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
ackFrames = buildHeartbeatAckFrames(
|
|
731
|
+
this.#createAuthProof("heartbeat_ack", "", [], keys.signKey),
|
|
732
|
+
);
|
|
733
|
+
} else {
|
|
734
|
+
// 心跳视为在线确认:必要时自动补注册
|
|
735
|
+
this.#ensureSlaveOnline(identityText, identity, { touch: true });
|
|
569
736
|
}
|
|
570
737
|
|
|
571
|
-
|
|
572
|
-
|
|
738
|
+
await this.#sendQueue
|
|
739
|
+
.enqueue("router", () =>
|
|
740
|
+
this.#sockets.router.send([identityToBuffer(identity), ...ackFrames]),
|
|
741
|
+
)
|
|
742
|
+
.catch(() => {});
|
|
573
743
|
return;
|
|
574
744
|
}
|
|
575
745
|
|
|
576
746
|
// ── 注册 ────────────────────────────────────────────────────────────────
|
|
577
747
|
if (parsed.kind === "register") {
|
|
578
748
|
if (this.#secureEnabled) {
|
|
749
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
750
|
+
if (!keys) {
|
|
751
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
752
|
+
if (this.#slaves.delete(identityText)) {
|
|
753
|
+
this.emit("slave_disconnected", identityText);
|
|
754
|
+
}
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
|
|
579
758
|
const v = this.#verifyIncomingProof({
|
|
580
759
|
kind: "register",
|
|
581
760
|
proofToken: parsed.authKey, // register 复用 authKey 字段承载 proof
|
|
582
761
|
requestId: "",
|
|
583
762
|
payloadFrames: [],
|
|
584
763
|
expectedNodeId: identityText,
|
|
764
|
+
signKey: keys.signKey,
|
|
585
765
|
});
|
|
586
766
|
if (!v.ok) {
|
|
587
767
|
this.#emitAuthFailed(event, v.error);
|
|
588
768
|
return;
|
|
589
769
|
}
|
|
770
|
+
|
|
771
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
772
|
+
touch: true,
|
|
773
|
+
signKey: keys.signKey,
|
|
774
|
+
encryptKey: keys.encryptKey,
|
|
775
|
+
});
|
|
776
|
+
return;
|
|
590
777
|
}
|
|
591
778
|
|
|
592
|
-
this.#slaves.set(identityText, {
|
|
779
|
+
this.#slaves.set(identityText, {
|
|
780
|
+
identity,
|
|
781
|
+
lastSeen: Date.now(),
|
|
782
|
+
signKey: null,
|
|
783
|
+
encryptKey: null,
|
|
784
|
+
});
|
|
593
785
|
this.emit("slave_connected", identityText);
|
|
594
786
|
return;
|
|
595
787
|
}
|
|
@@ -607,12 +799,22 @@ export class ZNL extends EventEmitter {
|
|
|
607
799
|
let finalFrames = parsed.payloadFrames;
|
|
608
800
|
|
|
609
801
|
if (this.#secureEnabled) {
|
|
802
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
803
|
+
if (!keys) {
|
|
804
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
805
|
+
if (this.#slaves.delete(identityText)) {
|
|
806
|
+
this.emit("slave_disconnected", identityText);
|
|
807
|
+
}
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
|
|
610
811
|
const v = this.#verifyIncomingProof({
|
|
611
812
|
kind: "request",
|
|
612
813
|
proofToken: parsed.authKey, // request 复用 authKey 字段承载 proof
|
|
613
814
|
requestId: parsed.requestId,
|
|
614
815
|
payloadFrames: parsed.payloadFrames,
|
|
615
816
|
expectedNodeId: identityText,
|
|
817
|
+
signKey: keys.signKey,
|
|
616
818
|
});
|
|
617
819
|
if (!v.ok) {
|
|
618
820
|
this.#emitAuthFailed(event, v.error);
|
|
@@ -620,7 +822,11 @@ export class ZNL extends EventEmitter {
|
|
|
620
822
|
}
|
|
621
823
|
|
|
622
824
|
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
623
|
-
this.#ensureSlaveOnline(identityText, identity, {
|
|
825
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
826
|
+
touch: true,
|
|
827
|
+
signKey: keys.signKey,
|
|
828
|
+
encryptKey: keys.encryptKey,
|
|
829
|
+
});
|
|
624
830
|
|
|
625
831
|
if (this.encrypted) {
|
|
626
832
|
try {
|
|
@@ -629,6 +835,7 @@ export class ZNL extends EventEmitter {
|
|
|
629
835
|
parsed.requestId,
|
|
630
836
|
parsed.payloadFrames,
|
|
631
837
|
identityText,
|
|
838
|
+
keys.encryptKey,
|
|
632
839
|
);
|
|
633
840
|
} catch (error) {
|
|
634
841
|
this.#emitAuthFailed(
|
|
@@ -663,12 +870,22 @@ export class ZNL extends EventEmitter {
|
|
|
663
870
|
let finalFrames = parsed.payloadFrames;
|
|
664
871
|
|
|
665
872
|
if (this.#secureEnabled) {
|
|
873
|
+
const keys = this.#resolveSlaveKeys(identityText);
|
|
874
|
+
if (!keys) {
|
|
875
|
+
this.#emitAuthFailed(event, "未配置该 slave 的 authKey。");
|
|
876
|
+
if (this.#slaves.delete(identityText)) {
|
|
877
|
+
this.emit("slave_disconnected", identityText);
|
|
878
|
+
}
|
|
879
|
+
return;
|
|
880
|
+
}
|
|
881
|
+
|
|
666
882
|
const v = this.#verifyIncomingProof({
|
|
667
883
|
kind: "response",
|
|
668
884
|
proofToken: parsed.authProof,
|
|
669
885
|
requestId: parsed.requestId,
|
|
670
886
|
payloadFrames: parsed.payloadFrames,
|
|
671
887
|
expectedNodeId: identityText,
|
|
888
|
+
signKey: keys.signKey,
|
|
672
889
|
});
|
|
673
890
|
if (!v.ok) {
|
|
674
891
|
this.#emitAuthFailed(event, v.error);
|
|
@@ -676,7 +893,11 @@ export class ZNL extends EventEmitter {
|
|
|
676
893
|
}
|
|
677
894
|
|
|
678
895
|
// 认证通过后允许补注册(避免 master 重启后丢失在线表)
|
|
679
|
-
this.#ensureSlaveOnline(identityText, identity, {
|
|
896
|
+
this.#ensureSlaveOnline(identityText, identity, {
|
|
897
|
+
touch: true,
|
|
898
|
+
signKey: keys.signKey,
|
|
899
|
+
encryptKey: keys.encryptKey,
|
|
900
|
+
});
|
|
680
901
|
|
|
681
902
|
if (this.encrypted) {
|
|
682
903
|
try {
|
|
@@ -685,6 +906,7 @@ export class ZNL extends EventEmitter {
|
|
|
685
906
|
parsed.requestId,
|
|
686
907
|
parsed.payloadFrames,
|
|
687
908
|
identityText,
|
|
909
|
+
keys.encryptKey,
|
|
688
910
|
);
|
|
689
911
|
} catch (error) {
|
|
690
912
|
this.#emitAuthFailed(
|
|
@@ -717,6 +939,28 @@ export class ZNL extends EventEmitter {
|
|
|
717
939
|
|
|
718
940
|
const event = this.#buildAndEmit("dealer", frames, { payload, ...parsed });
|
|
719
941
|
|
|
942
|
+
// ── 心跳应答:master -> slave ──────────────────────────────────────────
|
|
943
|
+
if (parsed.kind === "heartbeat_ack") {
|
|
944
|
+
if (this.#secureEnabled) {
|
|
945
|
+
const v = this.#verifyIncomingProof({
|
|
946
|
+
kind: "heartbeat_ack",
|
|
947
|
+
proofToken: parsed.authProof,
|
|
948
|
+
requestId: "",
|
|
949
|
+
payloadFrames: [],
|
|
950
|
+
expectedNodeId: this.#masterNodeId,
|
|
951
|
+
});
|
|
952
|
+
if (!v.ok) {
|
|
953
|
+
this.#emitAuthFailed(event, v.error);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (!this.#masterNodeId) this.#masterNodeId = v.envelope.nodeId;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
|
|
720
964
|
// ── PUB 广播:master -> slave ──────────────────────────────────────────
|
|
721
965
|
if (parsed.kind === "publish") {
|
|
722
966
|
let finalFrames = parsed.payloadFrames;
|
|
@@ -755,6 +999,8 @@ export class ZNL extends EventEmitter {
|
|
|
755
999
|
}
|
|
756
1000
|
}
|
|
757
1001
|
|
|
1002
|
+
this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
|
|
1003
|
+
|
|
758
1004
|
const finalPayload = payloadFromFrames(finalFrames);
|
|
759
1005
|
const pubEvent = { topic: parsed.topic, payload: finalPayload };
|
|
760
1006
|
|
|
@@ -808,6 +1054,8 @@ export class ZNL extends EventEmitter {
|
|
|
808
1054
|
}
|
|
809
1055
|
}
|
|
810
1056
|
|
|
1057
|
+
this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
|
|
1058
|
+
|
|
811
1059
|
const finalPayload = payloadFromFrames(finalFrames);
|
|
812
1060
|
const requestEvent = { ...event, payload: finalPayload };
|
|
813
1061
|
this.emit("request", requestEvent);
|
|
@@ -860,6 +1108,8 @@ export class ZNL extends EventEmitter {
|
|
|
860
1108
|
}
|
|
861
1109
|
}
|
|
862
1110
|
|
|
1111
|
+
this.#confirmMasterReachable({ scheduleNextHeartbeat: true });
|
|
1112
|
+
|
|
863
1113
|
const finalPayload = payloadFromFrames(finalFrames);
|
|
864
1114
|
const responseEvent = { ...event, payload: finalPayload };
|
|
865
1115
|
const key = this.#pending.key(parsed.requestId);
|
|
@@ -927,13 +1177,21 @@ export class ZNL extends EventEmitter {
|
|
|
927
1177
|
identityText,
|
|
928
1178
|
);
|
|
929
1179
|
|
|
1180
|
+
const keys = this.#secureEnabled
|
|
1181
|
+
? this.#resolveSlaveKeys(identityText)
|
|
1182
|
+
: null;
|
|
1183
|
+
if (this.#secureEnabled && !keys) {
|
|
1184
|
+
throw new Error(`未配置该 slave 的 authKey:${identityText}`);
|
|
1185
|
+
}
|
|
1186
|
+
|
|
930
1187
|
const payloadFrames = this.#sealPayloadFrames(
|
|
931
1188
|
"request",
|
|
932
1189
|
requestId,
|
|
933
1190
|
payload,
|
|
1191
|
+
keys?.encryptKey ?? null,
|
|
934
1192
|
);
|
|
935
1193
|
const proofOrAuthKey = this.#secureEnabled
|
|
936
|
-
? this.#createAuthProof("request", requestId, payloadFrames)
|
|
1194
|
+
? this.#createAuthProof("request", requestId, payloadFrames, keys.signKey)
|
|
937
1195
|
: "";
|
|
938
1196
|
|
|
939
1197
|
const frames = buildRequestFrames(requestId, payloadFrames, proofOrAuthKey);
|
|
@@ -981,15 +1239,29 @@ export class ZNL extends EventEmitter {
|
|
|
981
1239
|
*/
|
|
982
1240
|
async #replyTo(identity, requestId, payload) {
|
|
983
1241
|
const socket = this.#requireSocket("router", "ROUTER");
|
|
1242
|
+
const identityText = identityToString(identity);
|
|
984
1243
|
const idFrame = identityToBuffer(identity);
|
|
985
1244
|
|
|
1245
|
+
const keys = this.#secureEnabled
|
|
1246
|
+
? this.#resolveSlaveKeys(identityText)
|
|
1247
|
+
: null;
|
|
1248
|
+
if (this.#secureEnabled && !keys) {
|
|
1249
|
+
throw new Error(`未配置该 slave 的 authKey:${identityText}`);
|
|
1250
|
+
}
|
|
1251
|
+
|
|
986
1252
|
const payloadFrames = this.#sealPayloadFrames(
|
|
987
1253
|
"response",
|
|
988
1254
|
requestId,
|
|
989
1255
|
payload,
|
|
1256
|
+
keys?.encryptKey ?? null,
|
|
990
1257
|
);
|
|
991
1258
|
const authProof = this.#secureEnabled
|
|
992
|
-
? this.#createAuthProof(
|
|
1259
|
+
? this.#createAuthProof(
|
|
1260
|
+
"response",
|
|
1261
|
+
requestId,
|
|
1262
|
+
payloadFrames,
|
|
1263
|
+
keys.signKey,
|
|
1264
|
+
)
|
|
993
1265
|
: "";
|
|
994
1266
|
|
|
995
1267
|
const frames = buildResponseFrames(requestId, payloadFrames, authProof);
|
|
@@ -1003,27 +1275,235 @@ export class ZNL extends EventEmitter {
|
|
|
1003
1275
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1004
1276
|
|
|
1005
1277
|
/**
|
|
1006
|
-
*
|
|
1278
|
+
* 创建并初始化 slave 侧 Dealer socket
|
|
1279
|
+
* 设计要点:
|
|
1280
|
+
* - immediate=true:仅向已完成连接的 pipe 发送,避免离线期间无限积压旧消息
|
|
1281
|
+
* - linger=0 :关闭旧 socket 时直接丢弃残留发送队列,避免旧心跳回灌
|
|
1282
|
+
* - sendTimeout=0:尽力发送,当前不可发时立即失败,不阻塞重连/启动流程
|
|
1283
|
+
*
|
|
1284
|
+
* @returns {import("zeromq").Dealer}
|
|
1285
|
+
*/
|
|
1286
|
+
#createDealerSocket() {
|
|
1287
|
+
const dealer = new zmq.Dealer({
|
|
1288
|
+
routingId: this.id,
|
|
1289
|
+
immediate: true,
|
|
1290
|
+
linger: 0,
|
|
1291
|
+
sendTimeout: 0,
|
|
1292
|
+
reconnectInterval: 200,
|
|
1293
|
+
reconnectMaxInterval: 1000,
|
|
1294
|
+
});
|
|
1295
|
+
|
|
1296
|
+
dealer.connect(this.endpoints.router);
|
|
1297
|
+
this.#consume(dealer, (frames) => this.#handleDealerFrames(frames));
|
|
1298
|
+
return dealer;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* 尝试发送注册帧
|
|
1303
|
+
* 说明:
|
|
1304
|
+
* - 该操作是“尽力发送”,不阻塞启动流程
|
|
1305
|
+
* - master 未上线时允许静默失败,由后续 heartbeat_ack 驱动恢复
|
|
1306
|
+
*/
|
|
1307
|
+
#trySendRegister() {
|
|
1308
|
+
if (this.role !== "slave" || !this.running || !this.#sockets.dealer) return;
|
|
1309
|
+
|
|
1310
|
+
const registerToken = this.#secureEnabled
|
|
1311
|
+
? this.#createAuthProof("register", "", [])
|
|
1312
|
+
: "";
|
|
1313
|
+
|
|
1314
|
+
this.#sendQueue
|
|
1315
|
+
.enqueue("dealer", () =>
|
|
1316
|
+
this.#sockets.dealer.send(buildRegisterFrames(registerToken)),
|
|
1317
|
+
)
|
|
1318
|
+
.catch(() => {});
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* 标记主节点已确认在线
|
|
1323
|
+
*/
|
|
1324
|
+
#markMasterOnline() {
|
|
1325
|
+
this.#masterOnline = true;
|
|
1326
|
+
this.#lastMasterSeenAt = Date.now();
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
/**
|
|
1330
|
+
* 标记主节点离线,并清理当前等待中的心跳状态
|
|
1331
|
+
*/
|
|
1332
|
+
#markMasterOffline() {
|
|
1333
|
+
this.#masterOnline = false;
|
|
1334
|
+
this.#lastMasterSeenAt = 0;
|
|
1335
|
+
this.#heartbeatWaitingAck = false;
|
|
1336
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
1337
|
+
this.#heartbeatAckTimer = null;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/**
|
|
1341
|
+
* 确认链路已打通
|
|
1342
|
+
* - heartbeat_ack 是最直接的确认信号
|
|
1343
|
+
* - 其他来自 master 的合法业务帧同样证明链路可达
|
|
1344
|
+
*
|
|
1345
|
+
* @param {{ scheduleNextHeartbeat?: boolean }} [options]
|
|
1346
|
+
*/
|
|
1347
|
+
#confirmMasterReachable({ scheduleNextHeartbeat = false } = {}) {
|
|
1348
|
+
this.#markMasterOnline();
|
|
1349
|
+
|
|
1350
|
+
if (!this.#heartbeatWaitingAck) return;
|
|
1351
|
+
|
|
1352
|
+
this.#heartbeatWaitingAck = false;
|
|
1353
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
1354
|
+
this.#heartbeatAckTimer = null;
|
|
1355
|
+
|
|
1356
|
+
if (scheduleNextHeartbeat && this.#heartbeatInterval > 0) {
|
|
1357
|
+
this.#scheduleNextHeartbeat();
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* 计算心跳应答超时时间
|
|
1363
|
+
* - 优先复用用户配置的 heartbeatTimeoutMs
|
|
1364
|
+
* - 未配置时回退到 interval × 2,且至少 1000ms
|
|
1365
|
+
*/
|
|
1366
|
+
#resolveHeartbeatAckTimeoutMs() {
|
|
1367
|
+
if (this.#heartbeatTimeoutMs > 0) return this.#heartbeatTimeoutMs;
|
|
1368
|
+
return Math.max(this.#heartbeatInterval * 2, 1000);
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
/**
|
|
1372
|
+
* 调度下一次心跳发送(单飞模式)
|
|
1373
|
+
* - 任意时刻最多只允许一个未确认心跳在飞
|
|
1374
|
+
* - 只有收到 heartbeat_ack 或其他合法回流后,才会进入下一轮
|
|
1375
|
+
*
|
|
1376
|
+
* @param {number} [delayMs=this.#heartbeatInterval]
|
|
1377
|
+
*/
|
|
1378
|
+
#scheduleNextHeartbeat(delayMs = this.#heartbeatInterval) {
|
|
1379
|
+
clearTimeout(this.#heartbeatTimer);
|
|
1380
|
+
this.#heartbeatTimer = setTimeout(
|
|
1381
|
+
() => {
|
|
1382
|
+
this.#sendHeartbeatOnce().catch((error) => this.emit("error", error));
|
|
1383
|
+
},
|
|
1384
|
+
Math.max(0, Number(delayMs) || 0),
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
/**
|
|
1389
|
+
* 启动 slave 侧心跳发送流程
|
|
1390
|
+
* 实现方式:
|
|
1391
|
+
* - 启动时立即发送第一帧心跳
|
|
1392
|
+
* - 后续改为“发一条 → 等应答 → 再调度下一条”
|
|
1007
1393
|
*/
|
|
1008
1394
|
#startHeartbeat() {
|
|
1009
|
-
if (this.#heartbeatInterval <= 0) return;
|
|
1395
|
+
if (this.role !== "slave" || this.#heartbeatInterval <= 0) return;
|
|
1010
1396
|
|
|
1011
|
-
this.#
|
|
1012
|
-
|
|
1397
|
+
this.#heartbeatWaitingAck = false;
|
|
1398
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
1399
|
+
this.#heartbeatAckTimer = null;
|
|
1400
|
+
this.#scheduleNextHeartbeat(0);
|
|
1401
|
+
}
|
|
1013
1402
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1403
|
+
/**
|
|
1404
|
+
* 发送单次心跳,并进入等待应答状态
|
|
1405
|
+
*/
|
|
1406
|
+
async #sendHeartbeatOnce() {
|
|
1407
|
+
if (
|
|
1408
|
+
!this.running ||
|
|
1409
|
+
this.role !== "slave" ||
|
|
1410
|
+
this.#heartbeatInterval <= 0 ||
|
|
1411
|
+
!this.#sockets.dealer ||
|
|
1412
|
+
this.#heartbeatWaitingAck
|
|
1413
|
+
) {
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1017
1416
|
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
: [CONTROL_PREFIX, CONTROL_HEARTBEAT];
|
|
1417
|
+
const proof = this.#secureEnabled
|
|
1418
|
+
? this.#createAuthProof("heartbeat", "", [])
|
|
1419
|
+
: "";
|
|
1022
1420
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1421
|
+
// plain 模式仍保持历史帧结构 [CONTROL_PREFIX, CONTROL_HEARTBEAT]
|
|
1422
|
+
const frames = this.#secureEnabled
|
|
1423
|
+
? buildHeartbeatFrames(proof)
|
|
1424
|
+
: [CONTROL_PREFIX, CONTROL_HEARTBEAT];
|
|
1425
|
+
|
|
1426
|
+
this.#heartbeatWaitingAck = true;
|
|
1427
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
1428
|
+
this.#heartbeatAckTimer = setTimeout(() => {
|
|
1429
|
+
this.#onHeartbeatAckTimeout();
|
|
1430
|
+
}, this.#resolveHeartbeatAckTimeoutMs());
|
|
1431
|
+
|
|
1432
|
+
try {
|
|
1433
|
+
await this.#sendQueue.enqueue("dealer", () =>
|
|
1434
|
+
this.#sockets.dealer.send(frames),
|
|
1435
|
+
);
|
|
1436
|
+
} catch {
|
|
1437
|
+
this.#onHeartbeatAckTimeout();
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* 心跳应答超时处理:
|
|
1443
|
+
* - 将主节点状态置为离线
|
|
1444
|
+
* - 主动重建 Dealer,丢弃旧连接残留消息
|
|
1445
|
+
*/
|
|
1446
|
+
#onHeartbeatAckTimeout() {
|
|
1447
|
+
if (!this.running || this.role !== "slave" || !this.#heartbeatWaitingAck) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
this.#markMasterOffline();
|
|
1452
|
+
this.#restartDealer("heartbeat_ack_timeout").catch((error) =>
|
|
1453
|
+
this.emit("error", error),
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
/**
|
|
1458
|
+
* 重建 slave 侧 Dealer 连接
|
|
1459
|
+
* 设计目标:
|
|
1460
|
+
* - 避免 master 重启后旧 socket 中残留的心跳/请求继续回灌
|
|
1461
|
+
* - 将重连控制为单次串行过程,避免并发 close/connect 造成状态抖动
|
|
1462
|
+
*
|
|
1463
|
+
* @param {string} reason
|
|
1464
|
+
* @returns {Promise<void>}
|
|
1465
|
+
*/
|
|
1466
|
+
async #restartDealer(reason = "reconnect") {
|
|
1467
|
+
if (this.role !== "slave" || !this.running) return;
|
|
1468
|
+
if (this.#dealerReconnectPromise) return this.#dealerReconnectPromise;
|
|
1469
|
+
|
|
1470
|
+
this.#dealerReconnectPromise = (async () => {
|
|
1471
|
+
clearTimeout(this.#heartbeatTimer);
|
|
1472
|
+
clearTimeout(this.#heartbeatAckTimer);
|
|
1473
|
+
this.#heartbeatTimer = null;
|
|
1474
|
+
this.#heartbeatAckTimer = null;
|
|
1475
|
+
this.#heartbeatWaitingAck = false;
|
|
1476
|
+
this.#masterOnline = false;
|
|
1477
|
+
this.#lastMasterSeenAt = 0;
|
|
1478
|
+
|
|
1479
|
+
this.#pending.rejectAll(
|
|
1480
|
+
new Error(
|
|
1481
|
+
`与主节点的连接已重建(reason=${String(reason)}),待处理请求已取消。`,
|
|
1482
|
+
),
|
|
1483
|
+
);
|
|
1484
|
+
|
|
1485
|
+
const oldDealer = this.#sockets.dealer;
|
|
1486
|
+
delete this.#sockets.dealer;
|
|
1487
|
+
|
|
1488
|
+
// slave 侧发送队列只服务 dealer,重建时直接清空可避免旧任务继续写入旧 socket
|
|
1489
|
+
this.#sendQueue.clear();
|
|
1490
|
+
|
|
1491
|
+
if (oldDealer) {
|
|
1492
|
+
try {
|
|
1493
|
+
oldDealer.close();
|
|
1494
|
+
} catch {}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const dealer = this.#createDealerSocket();
|
|
1498
|
+
this.#sockets.dealer = dealer;
|
|
1499
|
+
|
|
1500
|
+
this.#trySendRegister();
|
|
1501
|
+
this.#startHeartbeat();
|
|
1502
|
+
})().finally(() => {
|
|
1503
|
+
this.#dealerReconnectPromise = null;
|
|
1504
|
+
});
|
|
1505
|
+
|
|
1506
|
+
return this.#dealerReconnectPromise;
|
|
1027
1507
|
}
|
|
1028
1508
|
|
|
1029
1509
|
/**
|
|
@@ -1056,13 +1536,14 @@ export class ZNL extends EventEmitter {
|
|
|
1056
1536
|
/**
|
|
1057
1537
|
* 生成签名证明令牌
|
|
1058
1538
|
*
|
|
1059
|
-
* @param {"register"|"heartbeat"|"request"|"response"|"publish"} kind
|
|
1539
|
+
* @param {"register"|"heartbeat"|"heartbeat_ack"|"request"|"response"|"publish"} kind
|
|
1060
1540
|
* @param {string} requestId
|
|
1061
1541
|
* @param {Buffer[]} payloadFrames
|
|
1062
1542
|
* @returns {string}
|
|
1063
1543
|
*/
|
|
1064
|
-
#createAuthProof(kind, requestId, payloadFrames) {
|
|
1065
|
-
|
|
1544
|
+
#createAuthProof(kind, requestId, payloadFrames, signKeyOverride = null) {
|
|
1545
|
+
const signKey = signKeyOverride ?? this.#signKey;
|
|
1546
|
+
if (!this.#secureEnabled || !signKey) return "";
|
|
1066
1547
|
|
|
1067
1548
|
const envelope = {
|
|
1068
1549
|
kind: String(kind),
|
|
@@ -1079,14 +1560,14 @@ export class ZNL extends EventEmitter {
|
|
|
1079
1560
|
// 保留 canonical 文本,便于后续排障(签名实际在 token 内完成)
|
|
1080
1561
|
canonicalSignInput(envelope);
|
|
1081
1562
|
|
|
1082
|
-
return encodeAuthProofToken(
|
|
1563
|
+
return encodeAuthProofToken(signKey, envelope);
|
|
1083
1564
|
}
|
|
1084
1565
|
|
|
1085
1566
|
/**
|
|
1086
1567
|
* 校验入站签名证明 + 防重放 + 摘要一致性
|
|
1087
1568
|
*
|
|
1088
1569
|
* @param {{
|
|
1089
|
-
* kind: "register"|"heartbeat"|"request"|"response"|"publish",
|
|
1570
|
+
* kind: "register"|"heartbeat"|"heartbeat_ack"|"request"|"response"|"publish",
|
|
1090
1571
|
* proofToken: string|null,
|
|
1091
1572
|
* requestId: string|null,
|
|
1092
1573
|
* payloadFrames: Buffer[],
|
|
@@ -1100,9 +1581,12 @@ export class ZNL extends EventEmitter {
|
|
|
1100
1581
|
requestId,
|
|
1101
1582
|
payloadFrames,
|
|
1102
1583
|
expectedNodeId = null,
|
|
1584
|
+
signKey = null,
|
|
1103
1585
|
}) {
|
|
1104
1586
|
if (!this.#secureEnabled) return { ok: true, envelope: null };
|
|
1105
|
-
|
|
1587
|
+
|
|
1588
|
+
const verifyKey = signKey ?? this.#signKey;
|
|
1589
|
+
if (!verifyKey) {
|
|
1106
1590
|
return { ok: false, error: "签名密钥未初始化。" };
|
|
1107
1591
|
}
|
|
1108
1592
|
|
|
@@ -1110,7 +1594,7 @@ export class ZNL extends EventEmitter {
|
|
|
1110
1594
|
return { ok: false, error: "缺少认证证明(proofToken)。" };
|
|
1111
1595
|
}
|
|
1112
1596
|
|
|
1113
|
-
const decoded = decodeAuthProofToken(
|
|
1597
|
+
const decoded = decodeAuthProofToken(verifyKey, proofToken, {
|
|
1114
1598
|
maxSkewMs: this.#maxTimeSkewMs,
|
|
1115
1599
|
now: Date.now(),
|
|
1116
1600
|
});
|
|
@@ -1167,13 +1651,14 @@ export class ZNL extends EventEmitter {
|
|
|
1167
1651
|
* @param {*} payload
|
|
1168
1652
|
* @returns {Buffer[]}
|
|
1169
1653
|
*/
|
|
1170
|
-
#sealPayloadFrames(kind, requestId, payload) {
|
|
1654
|
+
#sealPayloadFrames(kind, requestId, payload, encryptKeyOverride = null) {
|
|
1171
1655
|
if (!this.encrypted) {
|
|
1172
1656
|
// 非加密模式保持历史行为:沿用协议层的原始帧类型(string/Buffer)
|
|
1173
1657
|
return normalizeFrames(payload);
|
|
1174
1658
|
}
|
|
1175
1659
|
|
|
1176
|
-
|
|
1660
|
+
const encryptKey = encryptKeyOverride ?? this.#encryptKey;
|
|
1661
|
+
if (!encryptKey) {
|
|
1177
1662
|
throw new Error("加密密钥未初始化。");
|
|
1178
1663
|
}
|
|
1179
1664
|
|
|
@@ -1183,11 +1668,7 @@ export class ZNL extends EventEmitter {
|
|
|
1183
1668
|
"utf8",
|
|
1184
1669
|
);
|
|
1185
1670
|
|
|
1186
|
-
const { iv, ciphertext, tag } = encryptFrames(
|
|
1187
|
-
this.#encryptKey,
|
|
1188
|
-
rawFrames,
|
|
1189
|
-
aad,
|
|
1190
|
-
);
|
|
1671
|
+
const { iv, ciphertext, tag } = encryptFrames(encryptKey, rawFrames, aad);
|
|
1191
1672
|
|
|
1192
1673
|
// 用统一信封包装:version + iv + tag + ciphertext
|
|
1193
1674
|
return [Buffer.from(SECURITY_ENVELOPE_VERSION), iv, tag, ciphertext];
|
|
@@ -1204,9 +1685,16 @@ export class ZNL extends EventEmitter {
|
|
|
1204
1685
|
* @param {string} senderNodeId
|
|
1205
1686
|
* @returns {Buffer[]}
|
|
1206
1687
|
*/
|
|
1207
|
-
#openPayloadFrames(
|
|
1688
|
+
#openPayloadFrames(
|
|
1689
|
+
kind,
|
|
1690
|
+
requestId,
|
|
1691
|
+
payloadFrames,
|
|
1692
|
+
senderNodeId,
|
|
1693
|
+
encryptKeyOverride = null,
|
|
1694
|
+
) {
|
|
1208
1695
|
if (!this.encrypted) return payloadFrames;
|
|
1209
|
-
|
|
1696
|
+
const encryptKey = encryptKeyOverride ?? this.#encryptKey;
|
|
1697
|
+
if (!encryptKey) throw new Error("加密密钥未初始化。");
|
|
1210
1698
|
|
|
1211
1699
|
if (!Array.isArray(payloadFrames) || payloadFrames.length !== 4) {
|
|
1212
1700
|
throw new Error("加密信封格式非法:期望 4 帧。");
|
|
@@ -1222,7 +1710,7 @@ export class ZNL extends EventEmitter {
|
|
|
1222
1710
|
"utf8",
|
|
1223
1711
|
);
|
|
1224
1712
|
|
|
1225
|
-
return decryptFrames(
|
|
1713
|
+
return decryptFrames(encryptKey, iv, ciphertext, tag, aad);
|
|
1226
1714
|
}
|
|
1227
1715
|
|
|
1228
1716
|
/**
|
|
@@ -1247,7 +1735,11 @@ export class ZNL extends EventEmitter {
|
|
|
1247
1735
|
* @param {Buffer|string|Uint8Array} identity
|
|
1248
1736
|
* @param {{ touch?: boolean }} [options]
|
|
1249
1737
|
*/
|
|
1250
|
-
#ensureSlaveOnline(
|
|
1738
|
+
#ensureSlaveOnline(
|
|
1739
|
+
identityText,
|
|
1740
|
+
identity,
|
|
1741
|
+
{ touch = true, signKey = null, encryptKey = null } = {},
|
|
1742
|
+
) {
|
|
1251
1743
|
const id = String(identityText ?? "");
|
|
1252
1744
|
if (!id) return;
|
|
1253
1745
|
|
|
@@ -1255,16 +1747,55 @@ export class ZNL extends EventEmitter {
|
|
|
1255
1747
|
const entry = this.#slaves.get(id);
|
|
1256
1748
|
if (entry) {
|
|
1257
1749
|
if (touch) entry.lastSeen = now;
|
|
1750
|
+
if (signKey) entry.signKey = signKey;
|
|
1751
|
+
if (encryptKey) entry.encryptKey = encryptKey;
|
|
1258
1752
|
return;
|
|
1259
1753
|
}
|
|
1260
1754
|
|
|
1261
1755
|
this.#slaves.set(id, {
|
|
1262
1756
|
identity: identityToBuffer(identity),
|
|
1263
1757
|
lastSeen: now,
|
|
1758
|
+
signKey: signKey ?? null,
|
|
1759
|
+
encryptKey: encryptKey ?? null,
|
|
1264
1760
|
});
|
|
1265
1761
|
this.emit("slave_connected", id);
|
|
1266
1762
|
}
|
|
1267
1763
|
|
|
1764
|
+
/**
|
|
1765
|
+
* master 侧解析某个 slave 的签名/加密密钥
|
|
1766
|
+
* - 优先匹配 authKeyMap
|
|
1767
|
+
* - 未命中时回退到 this.authKey(若提供)
|
|
1768
|
+
*
|
|
1769
|
+
* @param {string} slaveId
|
|
1770
|
+
* @returns {{ signKey: Buffer, encryptKey: Buffer }|null}
|
|
1771
|
+
*/
|
|
1772
|
+
#resolveSlaveKeys(slaveId) {
|
|
1773
|
+
if (!this.#secureEnabled) return null;
|
|
1774
|
+
|
|
1775
|
+
const id = String(slaveId ?? "");
|
|
1776
|
+
if (!id) return null;
|
|
1777
|
+
|
|
1778
|
+
const cached = this.#slaveKeyCache.get(id);
|
|
1779
|
+
if (cached) return cached;
|
|
1780
|
+
|
|
1781
|
+
let key = this.#authKeyMap.get(id);
|
|
1782
|
+
if (!key && this.authKey) {
|
|
1783
|
+
key = this.authKey;
|
|
1784
|
+
}
|
|
1785
|
+
if (!key) return null;
|
|
1786
|
+
|
|
1787
|
+
const derived = deriveKeys(key);
|
|
1788
|
+
this.#slaveKeyCache.set(id, derived);
|
|
1789
|
+
|
|
1790
|
+
const entry = this.#slaves.get(id);
|
|
1791
|
+
if (entry) {
|
|
1792
|
+
entry.signKey = derived.signKey;
|
|
1793
|
+
entry.encryptKey = derived.encryptKey;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
return derived;
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1268
1799
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
1269
1800
|
// 工具方法
|
|
1270
1801
|
// ═══════════════════════════════════════════════════════════════════════════
|