@wenbin_wb/dsh-bridge 2.8.7 → 2.9.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.
@@ -2,12 +2,19 @@
2
2
  import { WebSocket } from 'ws';
3
3
  import { request as httpRequest } from 'node:http';
4
4
  import { connect as netConnect } from 'node:net';
5
- import { gzipSync } from 'node:zlib';
5
+ import { promisify } from 'node:util';
6
+ import { gzip as gzipCallback } from 'node:zlib';
7
+
8
+ const gzipAsync = promisify(gzipCallback);
6
9
 
7
10
  const HEARTBEAT_INTERVAL = 30000;
8
11
  const RECONNECT_DELAY = 5000;
9
12
  const MAX_RECONNECT_ATTEMPTS = 5;
10
13
 
14
+ // 单响应内存上限:隧道把整个 body 缓冲进内存再 base64 传输,无上限会在大文件
15
+ // 下载时把进程内存拖垮(一份 body 三份内存:chunks + Buffer + base64 字符串)
16
+ const MAX_RESPONSE_BYTES = 32 * 1024 * 1024; // 32MB
17
+
11
18
  // 大响应 gzip 压缩阈值(超过此大小的可压缩响应将被 gzip)
12
19
  const GZIP_THRESHOLD = 102400; // 100KB
13
20
  // 可压缩的 content-type 前缀
@@ -215,15 +222,31 @@ export class CustomTunnelClient {
215
222
  return;
216
223
  }
217
224
 
218
- res.on('data', (c) => chunks.push(c));
219
- res.on('error', () => {
225
+ let responded = false;
226
+ const sendResponse = (statusCode, headers, body) => {
227
+ if (responded) return;
228
+ responded = true;
220
229
  this._sendMessage({
221
- type: 'response', requestId, statusCode: 502,
222
- headers: { 'content-type': 'text/plain' },
223
- body: Buffer.from('Response Error').toString('base64'),
230
+ type: 'response', requestId, statusCode, headers,
231
+ body: Buffer.from(body).toString('base64'),
224
232
  });
233
+ };
234
+
235
+ let total = 0;
236
+ res.on('data', (c) => {
237
+ chunks.push(c);
238
+ total += c.length;
239
+ if (total > MAX_RESPONSE_BYTES) {
240
+ sendResponse(502, { 'content-type': 'text/plain' },
241
+ `Response too large for tunnel (>${Math.round(MAX_RESPONSE_BYTES / 1024 / 1024)}MB cap)`);
242
+ res.destroy();
243
+ }
244
+ });
245
+ res.on('error', () => {
246
+ sendResponse(502, { 'content-type': 'text/plain' }, 'Response Error');
225
247
  });
226
248
  res.on('end', () => {
249
+ if (responded) return;
227
250
  const respHeaders = Object.fromEntries(
228
251
  Object.entries(res.headers).filter(([k]) => !SKIP.has(k.toLowerCase()))
229
252
  );
@@ -249,21 +272,22 @@ export class CustomTunnelClient {
249
272
  } catch {} // 解析失败则原样发送
250
273
  }
251
274
 
252
- // 大响应 gzip 压缩:减少隧道 WebSocket 传输量
275
+ // 大响应 gzip 压缩:异步执行,避免 gzipSync 卡住整个事件循环
253
276
  const respCt = String(res.headers['content-type'] ?? '').toLowerCase();
254
277
  const alreadyEncoded = String(res.headers['content-encoding'] ?? '').toLowerCase();
255
278
  const compressible = COMPRESSIBLE_TYPES.some((t) => respCt.startsWith(t));
256
279
  if (bodyBuf.length > GZIP_THRESHOLD && compressible && !alreadyEncoded) {
257
- bodyBuf = gzipSync(bodyBuf);
258
- respHeaders['content-encoding'] = 'gzip';
259
- respHeaders['content-length'] = String(bodyBuf.length);
280
+ gzipAsync(bodyBuf).then((zipped) => {
281
+ respHeaders['content-encoding'] = 'gzip';
282
+ respHeaders['content-length'] = String(zipped.length);
283
+ sendResponse(res.statusCode, respHeaders, zipped);
284
+ }).catch(() => {
285
+ sendResponse(res.statusCode, respHeaders, bodyBuf);
286
+ });
287
+ return;
260
288
  }
261
289
 
262
- this._sendMessage({
263
- type: 'response', requestId,
264
- statusCode: res.statusCode, headers: respHeaders,
265
- body: bodyBuf.toString('base64'),
266
- });
290
+ sendResponse(res.statusCode, respHeaders, bodyBuf);
267
291
  });
268
292
  });
269
293
  req.on('error', err => {