@maiyunnet/kebab 9.17.3 → 9.18.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/lib/ws.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import * as net from 'net';
2
2
  // --- 第三方 ---
3
- import * as liws from '@litert/websocket';
3
+ import WebSocket, { WebSocketServer } from 'ws';
4
+ import * as lCore from '#kebab/lib/core.js';
4
5
  import * as lText from '#kebab/lib/text.js';
5
6
  import * as lUndici from '#kebab/lib/undici.js';
6
7
  import * as lCookie from '#kebab/lib/cookie.js';
@@ -21,8 +22,32 @@ export var EOpcode;
21
22
  EOpcode[EOpcode["PING"] = 9] = "PING";
22
23
  EOpcode[EOpcode["PONG"] = 10] = "PONG";
23
24
  })(EOpcode || (EOpcode = {}));
24
- const liwsServer = liws.createServer({
25
- 'frameReceiveMode': EFrameReceiveMode.SIMPLE,
25
+ /** --- 单条 WebSocket 消息最大 64 MiB,与原实现保持一致 --- */
26
+ const MAX_MESSAGE_BYTES = 64 * 1024 * 1024;
27
+ /** --- 服务端 Upgrade 响应头,按请求隔离以支持并发握手 --- */
28
+ const upgradeHeaders = new WeakMap();
29
+ /** --- Kebab 复用的无监听端口 WebSocket 服务端 --- */
30
+ const wsServer = new WebSocketServer({
31
+ 'allowSynchronousEvents': false,
32
+ 'autoPong': false,
33
+ 'clientTracking': false,
34
+ 'maxPayload': MAX_MESSAGE_BYTES,
35
+ 'noServer': true,
36
+ 'perMessageDeflate': false,
37
+ });
38
+ wsServer.on('headers', (headers, request) => {
39
+ const extra = upgradeHeaders.get(request);
40
+ if (!extra) {
41
+ return;
42
+ }
43
+ for (const [name, value] of Object.entries(extra)) {
44
+ if (value === undefined) {
45
+ continue;
46
+ }
47
+ for (const item of Array.isArray(value) ? value : [value]) {
48
+ headers.push(`${name}: ${item}`);
49
+ }
50
+ }
26
51
  });
27
52
  /** --- 未能及时转发的 WebSocket 消息最大缓存量,超过后关闭连接保护进程内存 --- */
28
53
  const MAX_PENDING_MESSAGE_BYTES = 8 * 1024 * 1024;
@@ -31,18 +56,44 @@ const MAX_PENDING_MESSAGES = 4_096;
31
56
  export class Socket {
32
57
  /** --- 当前的 ws 对象 --- */
33
58
  _ws;
59
+ /** --- 当前是否为服务端接入的连接 --- */
60
+ _isServer = false;
61
+ /** --- 客户端发出的帧是否掩码 --- */
62
+ _masking = true;
63
+ /** --- 等待底层写入回调的消息数量 --- */
64
+ _pendingWrites = 0;
65
+ /** --- 是否已经因发送缓存触发背压 --- */
66
+ _writeBlocked = false;
67
+ /** --- 是否已由本端主动结束写入 --- */
68
+ _finished = false;
69
+ /** --- 是否已由对端结束读取 --- */
70
+ _ended = false;
71
+ /** --- 底层 TCP Socket,用于设置和清理空闲超时 --- */
72
+ _transport;
73
+ /** --- 底层 TCP Socket 的超时监听器 --- */
74
+ _transportTimeout;
34
75
  constructor(request, socket, head, options = {}) {
35
76
  if (!request || !socket) {
36
77
  return;
37
78
  }
38
79
  // --- 一定是 server 模式 ---
39
- this._ws = liwsServer.accept({
40
- 'request': request,
41
- 'socket': socket,
42
- 'headers': options.headers,
43
- 'timeout': options.timeout,
44
- 'clientEarlyDataPayload': head,
45
- });
80
+ this._isServer = true;
81
+ this._masking = false;
82
+ const accepted = {};
83
+ upgradeHeaders.set(request, options.headers ?? {});
84
+ try {
85
+ wsServer.handleUpgrade(request, socket, head ?? Buffer.alloc(0), client => {
86
+ accepted.socket = client;
87
+ });
88
+ }
89
+ finally {
90
+ upgradeHeaders.delete(request);
91
+ }
92
+ if (!accepted.socket) {
93
+ throw new Error('WebSocket upgrade failed.');
94
+ }
95
+ this._ws = accepted.socket;
96
+ this._bindTransport(socket, options.timeout ?? 0);
46
97
  this._bindEvent();
47
98
  }
48
99
  /**
@@ -60,12 +111,15 @@ export class Socket {
60
111
  const timeout = opt.timeout ?? 10;
61
112
  const hosts = opt.hosts ?? {};
62
113
  const local = opt.local;
63
- const mode = opt.mode ?? EFrameReceiveMode.SIMPLE;
64
- const masking = opt.masking ?? true;
114
+ this._masking = opt.masking ?? true;
65
115
  const headers = {};
66
116
  if (opt.headers) {
67
117
  for (const key in opt.headers) {
68
- headers[key.toLowerCase()] = opt.headers[key];
118
+ const value = opt.headers[key];
119
+ if (value === undefined) {
120
+ continue;
121
+ }
122
+ headers[key.toLowerCase()] = Array.isArray(value) ? value.join(', ') : value.toString();
69
123
  }
70
124
  }
71
125
  headers['user-agent'] ??= 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36';
@@ -73,102 +127,171 @@ export class Socket {
73
127
  if (opt.cookie) {
74
128
  headers['cookie'] = lCookie.buildCookieQuery(opt.cookie, uri);
75
129
  }
76
- // --- ssl ---
77
- const isSsl = puri ?
78
- (puri.protocol === 'wss:' ? true : false) :
79
- (uri.protocol === 'wss:' ? true : false);
80
- if (typeof hosts === 'string' ? hosts : hosts[uri.hostname]) {
81
- // --- 要设置额外的 host ---
82
- headers['host'] = uri.hostname;
83
- if (uri.port) {
84
- if (lText.isIPv6(headers['host'])) {
85
- headers['host'] = `[${headers['host']}]`;
86
- }
87
- headers['host'] += ':' + uri.port;
88
- }
89
- }
90
130
  try {
91
- // --- 重定义 IP ---
131
+ /** --- 实际建立连接的 URL --- */
132
+ const connectUrl = new URL(puri ? opt.mproxy?.url ?? '' : u);
133
+ if (puri) {
134
+ connectUrl.searchParams.set('url', u);
135
+ connectUrl.searchParams.set('auth', opt.mproxy?.auth ?? '');
136
+ }
92
137
  const host = puri?.hostname ?? uri.hostname ?? '';
93
138
  /** --- 真正的连接远程 IP / HOST --- */
94
139
  const rhost = typeof hosts === 'string' ? hosts : hosts[host];
95
- const port = (puri ? puri.port : uri.port) ?? 443;
96
- const path = puri ? puri.path + (puri.path?.includes('?') ? '&' : '?') + lText.queryStringify({
97
- 'url': u,
98
- 'auth': opt.mproxy?.auth ?? ''
99
- }) : uri.path;
100
- const cli = isSsl ?
101
- liws.createSecureClient({
102
- 'hostname': rhost || host,
103
- 'port': port,
104
- 'path': path,
105
- 'servername': host,
106
- 'headers': headers,
107
- 'connectTimeout': timeout * 1000,
108
- 'frameReceiveMode': mode,
109
- 'localAddress': local,
110
- }) :
111
- liws.createClient({
112
- 'hostname': rhost || host,
113
- 'port': port,
114
- 'path': path,
115
- 'headers': headers,
116
- 'connectTimeout': timeout * 1000,
117
- 'frameReceiveMode': mode,
118
- 'localAddress': local,
119
- });
120
- cli.setMasking(masking);
140
+ if (rhost) {
141
+ headers['host'] ??= connectUrl.host;
142
+ connectUrl.hostname = lText.isIPv6(rhost) ? `[${rhost}]` : rhost;
143
+ }
144
+ const clientOptions = {
145
+ 'allowSynchronousEvents': false,
146
+ 'autoPong': false,
147
+ 'handshakeTimeout': timeout * 1000,
148
+ 'headers': headers,
149
+ 'localAddress': local,
150
+ 'maxPayload': MAX_MESSAGE_BYTES,
151
+ 'perMessageDeflate': false,
152
+ 'servername': connectUrl.protocol === 'wss:' ? host : undefined,
153
+ };
154
+ const cli = new WebSocket(connectUrl, clientOptions);
121
155
  this._ws = cli;
122
156
  this._bindEvent();
123
- await cli.connect();
157
+ await new Promise((resolve, reject) => {
158
+ let onOpen;
159
+ let onError;
160
+ let onUnexpectedResponse;
161
+ const clean = () => {
162
+ cli.off('open', onOpen);
163
+ cli.off('error', onError);
164
+ cli.off('unexpected-response', onUnexpectedResponse);
165
+ };
166
+ onOpen = () => {
167
+ clean();
168
+ resolve();
169
+ };
170
+ onError = (error) => {
171
+ clean();
172
+ reject(error);
173
+ };
174
+ onUnexpectedResponse = (_request, response) => {
175
+ clean();
176
+ response.resume();
177
+ cli.terminate();
178
+ reject(new Error(`WebSocket handshake failed: ${response.statusCode ?? 0} ${response.statusMessage ?? ''}`.trim()));
179
+ };
180
+ cli.once('open', onOpen);
181
+ cli.once('error', onError);
182
+ cli.once('unexpected-response', onUnexpectedResponse);
183
+ });
124
184
  return this;
125
185
  }
126
- catch {
186
+ catch (e) {
187
+ opt.onConnectError?.(e);
127
188
  return null;
128
189
  }
129
190
  }
130
191
  /** --- 创建成功后第一时间绑定事件 --- */
131
192
  _bindEvent() {
132
- this._ws.on('message', msg => {
133
- if (msg.opcode === EOpcode.CLOSE) {
134
- return;
135
- }
136
- if (!('data' in msg)) {
137
- return;
138
- }
139
- const item = {
140
- 'opcode': msg.opcode,
141
- 'data': Buffer.concat(msg.data),
142
- };
143
- if (this._paused || !this._on.message) {
144
- this._queueMessage(item);
145
- return;
146
- }
147
- this._on.message(item);
148
- }).on('drain', () => {
149
- this._on.drain?.();
193
+ this._ws.on('message', (data, isBinary) => {
194
+ this._receive({
195
+ 'opcode': isBinary ? EOpcode.BINARY : EOpcode.TEXT,
196
+ 'data': this._toBuffer(data),
197
+ });
198
+ }).on('ping', data => {
199
+ this._receive({
200
+ 'opcode': EOpcode.PING,
201
+ 'data': data,
202
+ });
203
+ }).on('pong', data => {
204
+ this._receive({
205
+ 'opcode': EOpcode.PONG,
206
+ 'data': data,
207
+ });
150
208
  }).on('error', (e) => {
151
- if (this._on.error) {
152
- this._on.error(e);
209
+ this._emitError(e);
210
+ }).on('close', (code, reason) => {
211
+ if (!this._finished) {
212
+ this._ended = true;
213
+ this._on.end?.();
153
214
  }
154
- else {
155
- this._error = e;
156
- }
157
- }).on('end', () => {
158
- this._on.end?.();
159
- }).on('close', () => {
215
+ this._finished = true;
216
+ this._ended = true;
160
217
  this._waitMsg.length = 0;
161
218
  this._waitMsgBytes = 0;
219
+ this._unbindTransport();
220
+ const info = {
221
+ 'code': code,
222
+ 'reason': reason.toString(),
223
+ };
162
224
  if (this._on.close) {
163
- this._on.close();
225
+ this._on.close(info);
164
226
  }
165
227
  else {
166
- this._close = true;
228
+ this._close = info;
167
229
  }
168
- }).on('timeout', () => {
169
- this._on.timeout?.();
170
230
  });
171
231
  }
232
+ /**
233
+ * --- 绑定底层 TCP Socket 的空闲超时 ---
234
+ * @param socket 底层 TCP Socket
235
+ * @param timeout 超时毫秒数,0 为不超时
236
+ */
237
+ _bindTransport(socket, timeout) {
238
+ this._transport = socket;
239
+ socket.setTimeout(timeout);
240
+ if (!timeout) {
241
+ return;
242
+ }
243
+ this._transportTimeout = () => {
244
+ this._on.timeout?.();
245
+ this.destroy();
246
+ };
247
+ socket.once('timeout', this._transportTimeout);
248
+ }
249
+ /** --- 清理底层 TCP Socket 监听器 --- */
250
+ _unbindTransport() {
251
+ if (this._transport && this._transportTimeout) {
252
+ this._transport.off('timeout', this._transportTimeout);
253
+ }
254
+ this._transport = undefined;
255
+ this._transportTimeout = undefined;
256
+ }
257
+ /**
258
+ * --- 将 ws 的消息数据统一转换为 Buffer ---
259
+ * @param data ws 原始消息数据
260
+ * @returns Buffer
261
+ */
262
+ _toBuffer(data) {
263
+ if (Buffer.isBuffer(data)) {
264
+ return data;
265
+ }
266
+ if (Array.isArray(data)) {
267
+ return Buffer.concat(data);
268
+ }
269
+ return Buffer.from(data);
270
+ }
271
+ /**
272
+ * --- 接收并派发消息;上层未就绪或主动暂停时同步暂停底层 TCP 读取 ---
273
+ * @param msg 消息
274
+ */
275
+ _receive(msg) {
276
+ if (this._paused || !this._on.message) {
277
+ this._queueMessage(msg);
278
+ this._ws.pause();
279
+ return;
280
+ }
281
+ this._on.message(msg);
282
+ }
283
+ /**
284
+ * --- 派发错误,监听器尚未绑定时暂存 ---
285
+ * @param error 错误对象
286
+ */
287
+ _emitError(error) {
288
+ if (this._on.error) {
289
+ this._on.error(error);
290
+ }
291
+ else {
292
+ this._error = error;
293
+ }
294
+ }
172
295
  /** --- 还未开启监听时来的数据将存在这里 --- */
173
296
  _waitMsg = [];
174
297
  /** --- 尚未交给消息监听器的数据量 --- */
@@ -178,7 +301,7 @@ export class Socket {
178
301
  /** --- 还未开启 error 监听时产生的 error 错误对象 --- */
179
302
  _error = null;
180
303
  /** --- 还未开启 close 监听时是不是就已经 close --- */
181
- _close = false;
304
+ _close;
182
305
  /** --- 绑定的自定义监听事件(未绑定则默认在 _bindEvent 执行) --- */
183
306
  _on = {
184
307
  /** --- 消息 --- */
@@ -197,8 +320,15 @@ export class Socket {
197
320
  this._waitMsgBytes += msg.data.length;
198
321
  if ((this._waitMsgBytes > MAX_PENDING_MESSAGE_BYTES) ||
199
322
  (this._waitMsg.length >= MAX_PENDING_MESSAGES)) {
323
+ const error = new Error(`WebSocket pending message queue overflow: ${this._waitMsgBytes} bytes, ${this._waitMsg.length + 1} messages.`);
200
324
  this._waitMsg.length = 0;
201
325
  this._waitMsgBytes = 0;
326
+ if (this._on.error) {
327
+ this._on.error(error);
328
+ }
329
+ else {
330
+ this._error = error;
331
+ }
202
332
  this.destroy();
203
333
  return;
204
334
  }
@@ -227,6 +357,9 @@ export class Socket {
227
357
  switch (event) {
228
358
  case 'message': {
229
359
  this._flushMessages();
360
+ if (!this._paused) {
361
+ this._ws.resume();
362
+ }
230
363
  break;
231
364
  }
232
365
  case 'error': {
@@ -237,18 +370,21 @@ export class Socket {
237
370
  break;
238
371
  }
239
372
  case 'end': {
240
- if (!this._ws.ended) {
373
+ if (!this._ended) {
241
374
  break;
242
375
  }
243
376
  cb();
244
377
  break;
245
378
  }
246
- default: {
247
- // --- drain, close, timeout ---
379
+ case 'close': {
248
380
  if (!this._close) {
249
381
  break;
250
382
  }
251
- cb();
383
+ cb(this._close);
384
+ break;
385
+ }
386
+ default: {
387
+ // --- drain, timeout ---
252
388
  }
253
389
  }
254
390
  return this;
@@ -258,79 +394,148 @@ export class Socket {
258
394
  this._on[event] = undefined;
259
395
  return this;
260
396
  }
261
- end() {
262
- this._ws.end();
397
+ /**
398
+ * --- 正常结束 WebSocket 连接 ---
399
+ * @param code WebSocket 关闭码
400
+ * @param reason 关闭原因,超过协议上限时自动安全截断
401
+ */
402
+ end(code = 1000, reason = '') {
403
+ if (this._ws.readyState !== WebSocket.OPEN) {
404
+ return;
405
+ }
406
+ this._finished = true;
407
+ this._ws.close(code, truncateCloseReason(reason));
263
408
  }
264
409
  destroy() {
265
- this._ws.destroy();
410
+ this._finished = true;
411
+ this._ws.terminate();
266
412
  }
267
- /** --- 暂停向消息监听器派发数据,底层仍只保留有界缓存 --- */
413
+ /** --- 暂停向消息监听器派发数据,并暂停底层 TCP 读取 --- */
268
414
  pause() {
269
415
  this._paused = true;
416
+ this._ws.pause();
270
417
  }
271
- /** --- 恢复向消息监听器派发数据 --- */
418
+ /** --- 恢复派发缓存消息,并恢复底层 TCP 读取 --- */
272
419
  resume() {
273
420
  this._paused = false;
274
421
  this._flushMessages();
422
+ if (!this._paused) {
423
+ this._ws.resume();
424
+ }
275
425
  }
276
426
  /** --- 发送文本 --- */
277
427
  writeText(data) {
278
- if (!this._ws.writable) {
279
- return false;
280
- }
281
- return this._ws.writeText(data);
428
+ return this._send(data, false);
282
429
  }
283
430
  /** --- 发送结果对象字符串 --- */
284
431
  writeResult(data) {
285
- if (!this._ws.writable) {
286
- return false;
287
- }
288
- return this._ws.writeText(lText.stringifyResult(data));
432
+ return this._send(lText.stringifyResult(data), false);
289
433
  }
290
434
  /** --- 发送二进制 --- */
291
435
  writeBinary(data) {
292
- if (!this._ws.writable) {
436
+ return this._send(data, true);
437
+ }
438
+ /**
439
+ * --- 发送消息并将 ws 的发送缓存转换为 Kebab 的背压布尔值 ---
440
+ * @param data 消息数据
441
+ * @param binary 是否为二进制消息
442
+ * @returns 是否已直接写入底层 Socket 缓存
443
+ */
444
+ _send(data, binary) {
445
+ if (!this.writable) {
446
+ return false;
447
+ }
448
+ const content = Array.isArray(data) ? Buffer.concat(data.map(item => Buffer.from(item))) : data;
449
+ ++this._pendingWrites;
450
+ try {
451
+ this._ws.send(content, {
452
+ 'binary': binary,
453
+ 'compress': false,
454
+ 'fin': true,
455
+ 'mask': this._masking,
456
+ }, error => {
457
+ --this._pendingWrites;
458
+ if (error) {
459
+ this._emitError(error);
460
+ }
461
+ this._emitDrain();
462
+ });
463
+ }
464
+ catch (error) {
465
+ --this._pendingWrites;
466
+ this._emitError(error);
293
467
  return false;
294
468
  }
295
- return this._ws.writeBinary(data);
469
+ if (this._ws.bufferedAmount > 0) {
470
+ this._writeBlocked = true;
471
+ return false;
472
+ }
473
+ return true;
474
+ }
475
+ /** --- 发送缓存完全排空后派发 drain --- */
476
+ _emitDrain() {
477
+ if (!this._writeBlocked || this._pendingWrites || this._ws.bufferedAmount) {
478
+ return;
479
+ }
480
+ this._writeBlocked = false;
481
+ this._on.drain?.();
296
482
  }
297
483
  /** --- 当前是否是可写状态 --- */
298
484
  get writable() {
299
- return this._ws.writable;
485
+ return !this._finished && (this._ws.readyState === WebSocket.OPEN);
300
486
  }
301
487
  /** --- 当前是否已经结束读取,并且无法继续读取 --- */
302
488
  get ended() {
303
- return this._ws.ended;
489
+ return this._ended;
304
490
  }
305
491
  /** --- 当前是否已经结束写入,并且无法继续写入 --- */
306
492
  get finished() {
307
- return this._ws.finished;
493
+ return this._finished || (this._ws.readyState >= WebSocket.CLOSING);
308
494
  }
309
495
  /**
310
496
  * --- 当前连接是不是服务器连接 ---
311
497
  */
312
498
  get isServer() {
313
- return this._ws.isServer;
499
+ return this._isServer;
314
500
  }
315
501
  /** --- 发送 ping --- */
316
502
  ping(data) {
317
- try {
318
- this._ws.ping(data);
319
- return true;
320
- }
321
- catch {
322
- return false;
323
- }
503
+ return this._sendControl('ping', data);
324
504
  }
325
505
  /** --- 发送 ping --- */
326
506
  pong(data) {
507
+ return this._sendControl('pong', data);
508
+ }
509
+ /**
510
+ * --- 发送控制帧 ---
511
+ * @param method 控制帧类型
512
+ * @param data 控制帧数据
513
+ * @returns 是否已直接写入底层 Socket 缓存
514
+ */
515
+ _sendControl(method, data) {
516
+ if (!this.writable) {
517
+ return false;
518
+ }
519
+ ++this._pendingWrites;
327
520
  try {
328
- this._ws.pong(data);
329
- return true;
521
+ this._ws[method](data, this._masking, error => {
522
+ --this._pendingWrites;
523
+ if (error) {
524
+ this._emitError(error);
525
+ }
526
+ this._emitDrain();
527
+ });
330
528
  }
331
- catch {
529
+ catch (error) {
530
+ --this._pendingWrites;
531
+ this._emitError(error);
332
532
  return false;
333
533
  }
534
+ if (this._ws.bufferedAmount > 0) {
535
+ this._writeBlocked = true;
536
+ return false;
537
+ }
538
+ return true;
334
539
  }
335
540
  }
336
541
  /**
@@ -350,24 +555,102 @@ export function connect(u, opt = {}) {
350
555
  export function createServer(request, socket, head, options = {}) {
351
556
  return new Socket(request, socket, head, options);
352
557
  }
558
+ /** --- WebSocket 关闭原因最大字节数,控制帧的另外 2 字节用于关闭码 --- */
559
+ const MAX_CLOSE_REASON_BYTES = 123;
560
+ /**
561
+ * --- 按 UTF-8 字节安全截断 WebSocket 关闭原因 ---
562
+ * @param reason 原始关闭原因
563
+ * @returns 可写入关闭帧的原因
564
+ */
565
+ function truncateCloseReason(reason) {
566
+ const data = Buffer.from(reason);
567
+ if (data.length <= MAX_CLOSE_REASON_BYTES) {
568
+ return reason;
569
+ }
570
+ let length = MAX_CLOSE_REASON_BYTES;
571
+ while (length && ((data[length] & 0xC0) === 0x80)) {
572
+ --length;
573
+ }
574
+ return data.subarray(0, length).toString();
575
+ }
576
+ /**
577
+ * --- 判断关闭码是否允许写入 WebSocket 关闭帧 ---
578
+ * @param code 关闭码
579
+ * @returns 是否有效
580
+ */
581
+ function isSendableCloseCode(code) {
582
+ if (code === undefined) {
583
+ return false;
584
+ }
585
+ if ((code === 1000) || ((code >= 3000) && (code <= 4999))) {
586
+ return true;
587
+ }
588
+ return (code >= 1001) && (code <= 1014) && ![1004, 1005, 1006].includes(code);
589
+ }
590
+ /**
591
+ * --- 获取可安全转发给对端的关闭原因,仅保留关闭帧中已有的内容 ---
592
+ * @param info 管道关闭信息
593
+ * @returns 关闭原因
594
+ */
595
+ function getPipeCloseReason(info) {
596
+ return info.reason ?? '';
597
+ }
353
598
  /**
354
599
  * --- 绑定 socket 管道 ---
355
600
  * @param s1 第一个 socket
356
601
  * @param s2 第二个 socket
357
602
  */
358
- function bindPipe(s1, s2) {
603
+ function bindPipe(s1, s2, closeReason) {
359
604
  return new Promise(resolve => {
360
605
  /** --- 是否已经完成关闭,防止双向 close 重复处理 --- */
361
606
  let closed = false;
362
- /** --- 两端一起销毁,避免半开连接继续占用资源 --- */
363
- const close = () => {
607
+ /** --- 来源侧关闭前最后收到的底层事件 --- */
608
+ let sourceEvent = 'close';
609
+ /** --- 目标侧关闭前最后收到的底层事件 --- */
610
+ let targetEvent = 'close';
611
+ /** --- 来源侧最后一个错误 --- */
612
+ let sourceError;
613
+ /** --- 目标侧最后一个错误 --- */
614
+ let targetError;
615
+ /** --- 来源侧 WebSocket 关闭信息 --- */
616
+ let sourceCloseInfo;
617
+ /** --- 目标侧 WebSocket 关闭信息 --- */
618
+ let targetCloseInfo;
619
+ /** --- 销毁已断开侧,并将原因通过关闭帧回传另一侧 --- */
620
+ const close = (side) => {
364
621
  if (closed) {
365
622
  return;
366
623
  }
367
624
  closed = true;
368
- s1.destroy();
369
- s2.destroy();
370
- resolve();
625
+ const info = side === 'source' ? {
626
+ 'side': side,
627
+ 'event': sourceEvent,
628
+ 'error': sourceError,
629
+ ...sourceCloseInfo,
630
+ } : {
631
+ 'side': side,
632
+ 'event': targetEvent,
633
+ 'error': targetError,
634
+ ...targetCloseInfo,
635
+ };
636
+ const code = isSendableCloseCode(info.code) ? info.code : 1011;
637
+ let reason;
638
+ try {
639
+ reason = closeReason?.(info) ?? getPipeCloseReason(info);
640
+ }
641
+ catch (error) {
642
+ lCore.log({}, `[WS][PIPE][CLOSE REASON ERROR] ${lText.stringifyError(error)}`, '-error');
643
+ reason = getPipeCloseReason(info);
644
+ }
645
+ if (side === 'source') {
646
+ s1.destroy();
647
+ s2.end(code, reason);
648
+ }
649
+ else {
650
+ s1.end(code, reason);
651
+ s2.destroy();
652
+ }
653
+ resolve(info);
371
654
  };
372
655
  // --- 监听发送端的 ---
373
656
  s1.on('message', (msg) => {
@@ -385,7 +668,7 @@ function bindPipe(s1, s2) {
385
668
  break;
386
669
  }
387
670
  case EOpcode.CLOSE: {
388
- close();
671
+ close('source');
389
672
  break;
390
673
  }
391
674
  case EOpcode.PING: {
@@ -404,7 +687,21 @@ function bindPipe(s1, s2) {
404
687
  // --- EOpcode.CONTINUATION ---
405
688
  }
406
689
  }
407
- }).on('close', close).on('drain', () => {
690
+ }).on('error', (error) => {
691
+ if (sourceEvent !== 'timeout') {
692
+ sourceEvent = 'error';
693
+ }
694
+ sourceError = error;
695
+ }).on('end', () => {
696
+ if (sourceEvent === 'close') {
697
+ sourceEvent = 'end';
698
+ }
699
+ }).on('timeout', () => {
700
+ sourceEvent = 'timeout';
701
+ }).on('close', (info) => {
702
+ sourceCloseInfo = info;
703
+ close('source');
704
+ }).on('drain', () => {
408
705
  s2.resume();
409
706
  });
410
707
  // --- 监听远程端的 ---
@@ -423,7 +720,7 @@ function bindPipe(s1, s2) {
423
720
  break;
424
721
  }
425
722
  case EOpcode.CLOSE: {
426
- close();
723
+ close('target');
427
724
  break;
428
725
  }
429
726
  case EOpcode.PING: {
@@ -442,7 +739,21 @@ function bindPipe(s1, s2) {
442
739
  // --- EOpcode.CONTINUATION ---
443
740
  }
444
741
  }
445
- }).on('close', close).on('drain', () => {
742
+ }).on('error', (error) => {
743
+ if (targetEvent !== 'timeout') {
744
+ targetEvent = 'error';
745
+ }
746
+ targetError = error;
747
+ }).on('end', () => {
748
+ if (targetEvent === 'close') {
749
+ targetEvent = 'end';
750
+ }
751
+ }).on('timeout', () => {
752
+ targetEvent = 'timeout';
753
+ }).on('close', (info) => {
754
+ targetCloseInfo = info;
755
+ close('target');
756
+ }).on('drain', () => {
446
757
  s1.resume();
447
758
  });
448
759
  });
@@ -494,14 +805,31 @@ export async function rproxy(ctr, url, opt = {}) {
494
805
  const headers = Object.assign(lUndici.filterHeaders(req.headers, undefined, opt.filter), opt.headers);
495
806
  // --- 发起请求 ---
496
807
  /** --- 远程端的双向 socket --- */
808
+ /** --- 目标连接的握手错误 --- */
809
+ let connectError;
497
810
  const rsocket = await connect(url, {
498
811
  ...opt,
499
- headers
812
+ headers,
813
+ 'onConnectError': (error) => {
814
+ connectError = error;
815
+ opt.onConnectError?.(error);
816
+ },
500
817
  });
501
818
  if (!rsocket) {
819
+ const target = lText.parseUrl(url);
820
+ const endpoint = `${target.protocol ?? 'ws:'}//${target.hostname ?? 'unknown'}${target.port ? `:${target.port}` : ''}`;
821
+ lCore.log(ctr, `[WS][RPROXY][TARGET CONNECT ERROR] ${endpoint}: ${lText.stringifyError(connectError)}`, '-error');
502
822
  return false;
503
823
  }
504
- await bindPipe(socket, rsocket);
824
+ const info = await bindPipe(socket, rsocket, opt.closeReason);
825
+ opt.onClose?.(info);
826
+ if ((info.side === 'target') || (info.event === 'error') || (info.event === 'timeout')) {
827
+ const target = lText.parseUrl(url);
828
+ const endpoint = `${target.protocol ?? 'ws:'}//${target.hostname ?? 'unknown'}${target.port ? `:${target.port}` : ''}`;
829
+ const error = info.error === undefined ? '' : `: ${lText.stringifyError(info.error)}`;
830
+ const close = info.code === undefined ? '' : ` code=${info.code}${info.reason ? ` reason=${info.reason}` : ''}`;
831
+ lCore.log(ctr, `[WS][RPROXY][${info.side.toUpperCase()} ${info.event.toUpperCase()}] ${endpoint}${close}${error}`, '-error');
832
+ }
505
833
  return true;
506
834
  }
507
835
  /**