@maiyunnet/kebab 9.17.2 → 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,24 +22,78 @@ 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,
26
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
+ }
51
+ });
52
+ /** --- 未能及时转发的 WebSocket 消息最大缓存量,超过后关闭连接保护进程内存 --- */
53
+ const MAX_PENDING_MESSAGE_BYTES = 8 * 1024 * 1024;
54
+ /** --- 未能及时转发的 WebSocket 消息最大条数,防止大量小帧占满内存 --- */
55
+ const MAX_PENDING_MESSAGES = 4_096;
27
56
  export class Socket {
28
57
  /** --- 当前的 ws 对象 --- */
29
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;
30
75
  constructor(request, socket, head, options = {}) {
31
76
  if (!request || !socket) {
32
77
  return;
33
78
  }
34
79
  // --- 一定是 server 模式 ---
35
- this._ws = liwsServer.accept({
36
- 'request': request,
37
- 'socket': socket,
38
- 'headers': options.headers,
39
- 'timeout': options.timeout,
40
- 'clientEarlyDataPayload': head,
41
- });
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);
42
97
  this._bindEvent();
43
98
  }
44
99
  /**
@@ -56,12 +111,15 @@ export class Socket {
56
111
  const timeout = opt.timeout ?? 10;
57
112
  const hosts = opt.hosts ?? {};
58
113
  const local = opt.local;
59
- const mode = opt.mode ?? EFrameReceiveMode.SIMPLE;
60
- const masking = opt.masking ?? true;
114
+ this._masking = opt.masking ?? true;
61
115
  const headers = {};
62
116
  if (opt.headers) {
63
117
  for (const key in opt.headers) {
64
- 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();
65
123
  }
66
124
  }
67
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';
@@ -69,110 +127,181 @@ export class Socket {
69
127
  if (opt.cookie) {
70
128
  headers['cookie'] = lCookie.buildCookieQuery(opt.cookie, uri);
71
129
  }
72
- // --- ssl ---
73
- const isSsl = puri ?
74
- (puri.protocol === 'wss:' ? true : false) :
75
- (uri.protocol === 'wss:' ? true : false);
76
- if (typeof hosts === 'string' ? hosts : hosts[uri.hostname]) {
77
- // --- 要设置额外的 host ---
78
- headers['host'] = uri.hostname;
79
- if (uri.port) {
80
- if (lText.isIPv6(headers['host'])) {
81
- headers['host'] = `[${headers['host']}]`;
82
- }
83
- headers['host'] += ':' + uri.port;
84
- }
85
- }
86
130
  try {
87
- // --- 重定义 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
+ }
88
137
  const host = puri?.hostname ?? uri.hostname ?? '';
89
138
  /** --- 真正的连接远程 IP / HOST --- */
90
139
  const rhost = typeof hosts === 'string' ? hosts : hosts[host];
91
- const port = (puri ? puri.port : uri.port) ?? 443;
92
- const path = puri ? puri.path + (puri.path?.includes('?') ? '&' : '?') + lText.queryStringify({
93
- 'url': u,
94
- 'auth': opt.mproxy?.auth ?? ''
95
- }) : uri.path;
96
- const cli = isSsl ?
97
- liws.createSecureClient({
98
- 'hostname': rhost || host,
99
- 'port': port,
100
- 'path': path,
101
- 'servername': host,
102
- 'headers': headers,
103
- 'connectTimeout': timeout * 1000,
104
- 'frameReceiveMode': mode,
105
- 'localAddress': local,
106
- }) :
107
- liws.createClient({
108
- 'hostname': rhost || host,
109
- 'port': port,
110
- 'path': path,
111
- 'headers': headers,
112
- 'connectTimeout': timeout * 1000,
113
- 'frameReceiveMode': mode,
114
- 'localAddress': local,
115
- });
116
- 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);
117
155
  this._ws = cli;
118
156
  this._bindEvent();
119
- 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
+ });
120
184
  return this;
121
185
  }
122
- catch {
186
+ catch (e) {
187
+ opt.onConnectError?.(e);
123
188
  return null;
124
189
  }
125
190
  }
126
191
  /** --- 创建成功后第一时间绑定事件 --- */
127
192
  _bindEvent() {
128
- this._ws.on('message', msg => {
129
- if (msg.opcode === EOpcode.CLOSE) {
130
- return;
131
- }
132
- if (!('data' in msg)) {
133
- return;
134
- }
135
- const buf = Buffer.concat(msg.data);
136
- if (this._on.message) {
137
- this._on.message({
138
- 'opcode': msg.opcode,
139
- 'data': buf,
140
- });
141
- }
142
- else {
143
- this._waitMsg.push({
144
- 'opcode': msg.opcode,
145
- 'data': buf,
146
- });
147
- }
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;
217
+ this._waitMsg.length = 0;
218
+ this._waitMsgBytes = 0;
219
+ this._unbindTransport();
220
+ const info = {
221
+ 'code': code,
222
+ 'reason': reason.toString(),
223
+ };
160
224
  if (this._on.close) {
161
- this._on.close();
225
+ this._on.close(info);
162
226
  }
163
227
  else {
164
- this._close = true;
228
+ this._close = info;
165
229
  }
166
- }).on('timeout', () => {
167
- this._on.timeout?.();
168
230
  });
169
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
+ }
170
295
  /** --- 还未开启监听时来的数据将存在这里 --- */
171
296
  _waitMsg = [];
297
+ /** --- 尚未交给消息监听器的数据量 --- */
298
+ _waitMsgBytes = 0;
299
+ /** --- 是否暂停向上层派发消息 --- */
300
+ _paused = false;
172
301
  /** --- 还未开启 error 监听时产生的 error 错误对象 --- */
173
302
  _error = null;
174
303
  /** --- 还未开启 close 监听时是不是就已经 close --- */
175
- _close = false;
304
+ _close;
176
305
  /** --- 绑定的自定义监听事件(未绑定则默认在 _bindEvent 执行) --- */
177
306
  _on = {
178
307
  /** --- 消息 --- */
@@ -183,12 +312,53 @@ export class Socket {
183
312
  end: undefined,
184
313
  timeout: undefined,
185
314
  };
315
+ /**
316
+ * --- 暂存尚不能交给上层处理的消息 ---
317
+ * @param msg 消息
318
+ */
319
+ _queueMessage(msg) {
320
+ this._waitMsgBytes += msg.data.length;
321
+ if ((this._waitMsgBytes > MAX_PENDING_MESSAGE_BYTES) ||
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.`);
324
+ this._waitMsg.length = 0;
325
+ this._waitMsgBytes = 0;
326
+ if (this._on.error) {
327
+ this._on.error(error);
328
+ }
329
+ else {
330
+ this._error = error;
331
+ }
332
+ this.destroy();
333
+ return;
334
+ }
335
+ this._waitMsg.push(msg);
336
+ }
337
+ /** --- 依次派发缓存消息,若再次发生背压则停止 --- */
338
+ _flushMessages() {
339
+ if (this._paused || !this._on.message || !this._waitMsg.length) {
340
+ return;
341
+ }
342
+ const messages = this._waitMsg.splice(0);
343
+ this._waitMsgBytes = 0;
344
+ for (let i = 0; i < messages.length; ++i) {
345
+ if (this._paused) {
346
+ for (; i < messages.length; ++i) {
347
+ this._waitMsg.push(messages[i]);
348
+ this._waitMsgBytes += messages[i].data.length;
349
+ }
350
+ return;
351
+ }
352
+ this._on.message(messages[i]);
353
+ }
354
+ }
186
355
  on(event, cb) {
187
356
  this._on[event] = cb;
188
357
  switch (event) {
189
358
  case 'message': {
190
- for (const item of this._waitMsg) {
191
- cb(item);
359
+ this._flushMessages();
360
+ if (!this._paused) {
361
+ this._ws.resume();
192
362
  }
193
363
  break;
194
364
  }
@@ -200,18 +370,21 @@ export class Socket {
200
370
  break;
201
371
  }
202
372
  case 'end': {
203
- if (!this._ws.ended) {
373
+ if (!this._ended) {
204
374
  break;
205
375
  }
206
376
  cb();
207
377
  break;
208
378
  }
209
- default: {
210
- // --- drain, close, timeout ---
379
+ case 'close': {
211
380
  if (!this._close) {
212
381
  break;
213
382
  }
214
- cb();
383
+ cb(this._close);
384
+ break;
385
+ }
386
+ default: {
387
+ // --- drain, timeout ---
215
388
  }
216
389
  }
217
390
  return this;
@@ -221,70 +394,148 @@ export class Socket {
221
394
  this._on[event] = undefined;
222
395
  return this;
223
396
  }
224
- end() {
225
- 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));
226
408
  }
227
409
  destroy() {
228
- this._ws.destroy();
410
+ this._finished = true;
411
+ this._ws.terminate();
412
+ }
413
+ /** --- 暂停向消息监听器派发数据,并暂停底层 TCP 读取 --- */
414
+ pause() {
415
+ this._paused = true;
416
+ this._ws.pause();
417
+ }
418
+ /** --- 恢复派发缓存消息,并恢复底层 TCP 读取 --- */
419
+ resume() {
420
+ this._paused = false;
421
+ this._flushMessages();
422
+ if (!this._paused) {
423
+ this._ws.resume();
424
+ }
229
425
  }
230
426
  /** --- 发送文本 --- */
231
427
  writeText(data) {
232
- if (!this._ws.writable) {
233
- return false;
234
- }
235
- return this._ws.writeText(data);
428
+ return this._send(data, false);
236
429
  }
237
430
  /** --- 发送结果对象字符串 --- */
238
431
  writeResult(data) {
239
- if (!this._ws.writable) {
240
- return false;
241
- }
242
- return this._ws.writeText(lText.stringifyResult(data));
432
+ return this._send(lText.stringifyResult(data), false);
243
433
  }
244
434
  /** --- 发送二进制 --- */
245
435
  writeBinary(data) {
246
- 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);
467
+ return false;
468
+ }
469
+ if (this._ws.bufferedAmount > 0) {
470
+ this._writeBlocked = true;
247
471
  return false;
248
472
  }
249
- return this._ws.writeBinary(data);
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?.();
250
482
  }
251
483
  /** --- 当前是否是可写状态 --- */
252
484
  get writable() {
253
- return this._ws.writable;
485
+ return !this._finished && (this._ws.readyState === WebSocket.OPEN);
254
486
  }
255
487
  /** --- 当前是否已经结束读取,并且无法继续读取 --- */
256
488
  get ended() {
257
- return this._ws.ended;
489
+ return this._ended;
258
490
  }
259
491
  /** --- 当前是否已经结束写入,并且无法继续写入 --- */
260
492
  get finished() {
261
- return this._ws.finished;
493
+ return this._finished || (this._ws.readyState >= WebSocket.CLOSING);
262
494
  }
263
495
  /**
264
496
  * --- 当前连接是不是服务器连接 ---
265
497
  */
266
498
  get isServer() {
267
- return this._ws.isServer;
499
+ return this._isServer;
268
500
  }
269
501
  /** --- 发送 ping --- */
270
502
  ping(data) {
271
- try {
272
- this._ws.ping(data);
273
- return true;
274
- }
275
- catch {
276
- return false;
277
- }
503
+ return this._sendControl('ping', data);
278
504
  }
279
505
  /** --- 发送 ping --- */
280
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;
281
520
  try {
282
- this._ws.pong(data);
283
- 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
+ });
528
+ }
529
+ catch (error) {
530
+ --this._pendingWrites;
531
+ this._emitError(error);
532
+ return false;
284
533
  }
285
- catch {
534
+ if (this._ws.bufferedAmount > 0) {
535
+ this._writeBlocked = true;
286
536
  return false;
287
537
  }
538
+ return true;
288
539
  }
289
540
  }
290
541
  /**
@@ -304,76 +555,206 @@ export function connect(u, opt = {}) {
304
555
  export function createServer(request, socket, head, options = {}) {
305
556
  return new Socket(request, socket, head, options);
306
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
+ }
307
598
  /**
308
599
  * --- 绑定 socket 管道 ---
309
600
  * @param s1 第一个 socket
310
601
  * @param s2 第二个 socket
311
602
  */
312
- function bindPipe(s1, s2) {
603
+ function bindPipe(s1, s2, closeReason) {
313
604
  return new Promise(resolve => {
605
+ /** --- 是否已经完成关闭,防止双向 close 重复处理 --- */
606
+ let closed = false;
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) => {
621
+ if (closed) {
622
+ return;
623
+ }
624
+ closed = true;
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);
654
+ };
314
655
  // --- 监听发送端的 ---
315
656
  s1.on('message', (msg) => {
316
657
  switch (msg.opcode) {
317
658
  case EOpcode.TEXT: {
318
- s2.writeText(msg.data.toString());
659
+ if (!s2.writeText(msg.data.toString())) {
660
+ s1.pause();
661
+ }
319
662
  break;
320
663
  }
321
664
  case EOpcode.BINARY: {
322
- s2.writeBinary(msg.data);
665
+ if (!s2.writeBinary(msg.data)) {
666
+ s1.pause();
667
+ }
323
668
  break;
324
669
  }
325
670
  case EOpcode.CLOSE: {
326
- s2.end();
327
- resolve();
671
+ close('source');
328
672
  break;
329
673
  }
330
674
  case EOpcode.PING: {
331
- s2.ping(msg.data);
675
+ if (!s2.ping(msg.data)) {
676
+ s1.pause();
677
+ }
332
678
  break;
333
679
  }
334
680
  case EOpcode.PONG: {
335
- s2.pong(msg.data);
681
+ if (!s2.pong(msg.data)) {
682
+ s1.pause();
683
+ }
336
684
  break;
337
685
  }
338
686
  default: {
339
687
  // --- EOpcode.CONTINUATION ---
340
688
  }
341
689
  }
342
- }).on('close', () => {
343
- s2.end();
344
- resolve();
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', () => {
705
+ s2.resume();
345
706
  });
346
707
  // --- 监听远程端的 ---
347
708
  s2.on('message', (msg) => {
348
709
  switch (msg.opcode) {
349
710
  case EOpcode.TEXT: {
350
- s1.writeText(msg.data.toString());
711
+ if (!s1.writeText(msg.data.toString())) {
712
+ s2.pause();
713
+ }
351
714
  break;
352
715
  }
353
716
  case EOpcode.BINARY: {
354
- s1.writeBinary(msg.data);
717
+ if (!s1.writeBinary(msg.data)) {
718
+ s2.pause();
719
+ }
355
720
  break;
356
721
  }
357
722
  case EOpcode.CLOSE: {
358
- s1.end();
359
- resolve();
723
+ close('target');
360
724
  break;
361
725
  }
362
726
  case EOpcode.PING: {
363
- s1.ping(msg.data);
727
+ if (!s1.ping(msg.data)) {
728
+ s2.pause();
729
+ }
364
730
  break;
365
731
  }
366
732
  case EOpcode.PONG: {
367
- s1.pong(msg.data);
733
+ if (!s1.pong(msg.data)) {
734
+ s2.pause();
735
+ }
368
736
  break;
369
737
  }
370
738
  default: {
371
739
  // --- EOpcode.CONTINUATION ---
372
740
  }
373
741
  }
374
- }).on('close', () => {
375
- s1.end();
376
- resolve();
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', () => {
757
+ s1.resume();
377
758
  });
378
759
  });
379
760
  }
@@ -424,14 +805,31 @@ export async function rproxy(ctr, url, opt = {}) {
424
805
  const headers = Object.assign(lUndici.filterHeaders(req.headers, undefined, opt.filter), opt.headers);
425
806
  // --- 发起请求 ---
426
807
  /** --- 远程端的双向 socket --- */
808
+ /** --- 目标连接的握手错误 --- */
809
+ let connectError;
427
810
  const rsocket = await connect(url, {
428
811
  ...opt,
429
- headers
812
+ headers,
813
+ 'onConnectError': (error) => {
814
+ connectError = error;
815
+ opt.onConnectError?.(error);
816
+ },
430
817
  });
431
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');
432
822
  return false;
433
823
  }
434
- 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
+ }
435
833
  return true;
436
834
  }
437
835
  /**