@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/doc/kebab-rag.md +5415 -2317
- package/index.d.ts +1 -1
- package/index.js +1 -1
- package/lib/socket.js +46 -19
- package/lib/undici/response.d.ts +1 -1
- package/lib/undici.js +10 -0
- package/lib/ws.d.ts +105 -3
- package/lib/ws.js +543 -145
- 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,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
|
-
|
|
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,
|
|
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.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
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',
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
this.
|
|
159
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
191
|
-
|
|
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.
|
|
373
|
+
if (!this._ended) {
|
|
204
374
|
break;
|
|
205
375
|
}
|
|
206
376
|
cb();
|
|
207
377
|
break;
|
|
208
378
|
}
|
|
209
|
-
|
|
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
|
-
|
|
225
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
485
|
+
return !this._finished && (this._ws.readyState === WebSocket.OPEN);
|
|
254
486
|
}
|
|
255
487
|
/** --- 当前是否已经结束读取,并且无法继续读取 --- */
|
|
256
488
|
get ended() {
|
|
257
|
-
return this.
|
|
489
|
+
return this._ended;
|
|
258
490
|
}
|
|
259
491
|
/** --- 当前是否已经结束写入,并且无法继续写入 --- */
|
|
260
492
|
get finished() {
|
|
261
|
-
return this._ws.
|
|
493
|
+
return this._finished || (this._ws.readyState >= WebSocket.CLOSING);
|
|
262
494
|
}
|
|
263
495
|
/**
|
|
264
496
|
* --- 当前连接是不是服务器连接 ---
|
|
265
497
|
*/
|
|
266
498
|
get isServer() {
|
|
267
|
-
return this.
|
|
499
|
+
return this._isServer;
|
|
268
500
|
}
|
|
269
501
|
/** --- 发送 ping --- */
|
|
270
502
|
ping(data) {
|
|
271
|
-
|
|
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
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
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('
|
|
375
|
-
|
|
376
|
-
|
|
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
|
/**
|