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