@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/doc/kebab-rag.md +5733 -2663
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/ws.d.ts +92 -5
- package/lib/ws.js +460 -132
- package/package.json +17 -16
- package/sys/ctr.d.ts +3 -1
- package/sys/ctr.js +4 -2
- package/sys/route.js +5 -7
package/lib/ws.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
2
|
// --- 第三方 ---
|
|
3
|
-
import
|
|
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
|
-
|
|
25
|
-
|
|
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.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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',
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
'
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
152
|
-
|
|
209
|
+
this._emitError(e);
|
|
210
|
+
}).on('close', (code, reason) => {
|
|
211
|
+
if (!this._finished) {
|
|
212
|
+
this._ended = true;
|
|
213
|
+
this._on.end?.();
|
|
153
214
|
}
|
|
154
|
-
|
|
155
|
-
|
|
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 =
|
|
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
|
|
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.
|
|
373
|
+
if (!this._ended) {
|
|
241
374
|
break;
|
|
242
375
|
}
|
|
243
376
|
cb();
|
|
244
377
|
break;
|
|
245
378
|
}
|
|
246
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
485
|
+
return !this._finished && (this._ws.readyState === WebSocket.OPEN);
|
|
300
486
|
}
|
|
301
487
|
/** --- 当前是否已经结束读取,并且无法继续读取 --- */
|
|
302
488
|
get ended() {
|
|
303
|
-
return this.
|
|
489
|
+
return this._ended;
|
|
304
490
|
}
|
|
305
491
|
/** --- 当前是否已经结束写入,并且无法继续写入 --- */
|
|
306
492
|
get finished() {
|
|
307
|
-
return this._ws.
|
|
493
|
+
return this._finished || (this._ws.readyState >= WebSocket.CLOSING);
|
|
308
494
|
}
|
|
309
495
|
/**
|
|
310
496
|
* --- 当前连接是不是服务器连接 ---
|
|
311
497
|
*/
|
|
312
498
|
get isServer() {
|
|
313
|
-
return this.
|
|
499
|
+
return this._isServer;
|
|
314
500
|
}
|
|
315
501
|
/** --- 发送 ping --- */
|
|
316
502
|
ping(data) {
|
|
317
|
-
|
|
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
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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('
|
|
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('
|
|
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
|
/**
|