@dainprotocol/tunnel 1.1.29 → 1.1.31

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.
@@ -270,8 +270,16 @@ class DainTunnel extends events_1.EventEmitter {
270
270
  this.safeSend({ type: 'websocket', id: message.id, event, data });
271
271
  };
272
272
  try {
273
+ // Preserve original host for JWT audience validation
274
+ const headers = { ...message.headers };
275
+ const originalHost = message.headers.host;
276
+ if (originalHost && !headers['x-forwarded-host']) {
277
+ headers['x-forwarded-host'] = originalHost;
278
+ headers['x-forwarded-proto'] = 'https';
279
+ }
280
+ delete headers.host; // Remove so WebSocket library doesn't send tunnel host to localhost
273
281
  const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
274
- headers: message.headers
282
+ headers
275
283
  });
276
284
  this.webSocketClients.set(message.id, client);
277
285
  client.on('message', (data) => {
@@ -315,6 +323,12 @@ class DainTunnel extends events_1.EventEmitter {
315
323
  };
316
324
  try {
317
325
  const headers = { ...message.headers };
326
+ // Preserve original host for JWT audience validation
327
+ const originalHost = message.headers.host;
328
+ if (originalHost && !headers['x-forwarded-host']) {
329
+ headers['x-forwarded-host'] = originalHost;
330
+ headers['x-forwarded-proto'] = 'https';
331
+ }
318
332
  delete headers.host;
319
333
  headers['accept-encoding'] = 'identity';
320
334
  const req = http_1.default.request({
@@ -339,28 +353,31 @@ class DainTunnel extends events_1.EventEmitter {
339
353
  sendSseEvent('connected');
340
354
  let buffer = '';
341
355
  const processBuffer = () => {
342
- buffer = buffer.replace(/\r\n/g, '\n');
343
- while (buffer.includes('\n\n')) {
344
- const idx = buffer.indexOf('\n\n');
345
- const msgData = buffer.substring(0, idx);
346
- buffer = buffer.substring(idx + 2);
347
- if (!msgData.trim() || msgData.startsWith(':'))
356
+ // Normalize line endings once
357
+ if (buffer.indexOf('\r') >= 0) {
358
+ buffer = buffer.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
359
+ }
360
+ let idx;
361
+ while ((idx = buffer.indexOf('\n\n')) >= 0) {
362
+ const msgData = buffer.slice(0, idx);
363
+ buffer = buffer.slice(idx + 2);
364
+ // Skip empty messages and comments
365
+ if (!msgData || msgData[0] === ':')
348
366
  continue;
349
367
  let event = 'message';
350
368
  let data = '';
351
- for (const line of msgData.split('\n')) {
352
- if (line.startsWith('event:')) {
353
- event = line.substring(6).trim();
369
+ const lines = msgData.split('\n');
370
+ for (let i = 0; i < lines.length; i++) {
371
+ const line = lines[i];
372
+ if (line.length > 6 && line[0] === 'e' && line.startsWith('event:')) {
373
+ event = line.slice(6).trim();
354
374
  }
355
- else if (line.startsWith('data:')) {
356
- data += line.substring(5).trim() + '\n';
375
+ else if (line.length > 5 && line[0] === 'd' && line.startsWith('data:')) {
376
+ data += (data ? '\n' : '') + line.slice(5).trim();
357
377
  }
358
378
  }
359
- if (data.endsWith('\n'))
360
- data = data.slice(0, -1);
361
- if (data) {
379
+ if (data)
362
380
  sendSseEvent(event, data);
363
- }
364
381
  }
365
382
  };
366
383
  res.on('data', (chunk) => {
@@ -419,12 +436,20 @@ class DainTunnel extends events_1.EventEmitter {
419
436
  resolve(response);
420
437
  }
421
438
  };
439
+ // Preserve original host for JWT audience validation
440
+ const headers = { ...request.headers };
441
+ const originalHost = request.headers.host;
442
+ if (originalHost && !headers['x-forwarded-host']) {
443
+ headers['x-forwarded-host'] = originalHost;
444
+ headers['x-forwarded-proto'] = 'https';
445
+ }
446
+ delete headers.host;
422
447
  const options = {
423
448
  hostname: 'localhost',
424
449
  port: this.port,
425
450
  path: request.path,
426
451
  method: request.method,
427
- headers: request.headers,
452
+ headers,
428
453
  agent: this.httpAgent,
429
454
  timeout: TIMEOUTS.REQUEST,
430
455
  };
@@ -9,13 +9,12 @@ const ws_1 = __importDefault(require("ws"));
9
9
  const body_parser_1 = __importDefault(require("body-parser"));
10
10
  const auth_1 = require("@dainprotocol/service-sdk/service/auth");
11
11
  const crypto_1 = require("crypto");
12
- const url_1 = require("url");
13
12
  const TIMEOUTS = {
14
13
  CHALLENGE_TTL: 30000,
15
14
  PING_INTERVAL: 30000,
16
15
  REQUEST_TIMEOUT: 30000,
17
16
  SSE_KEEPALIVE: 5000,
18
- TUNNEL_RETRY_DELAY: 100,
17
+ TUNNEL_RETRY_DELAY: 20, // Reduced from 100ms
19
18
  SERVER_KEEPALIVE: 65000,
20
19
  SERVER_HEADERS: 66000,
21
20
  };
@@ -26,11 +25,21 @@ const LIMITS = {
26
25
  BACKPRESSURE_THRESHOLD: 1024 * 1024,
27
26
  SERVER_MAX_HEADERS: 100,
28
27
  WS_BACKLOG: 100,
29
- TUNNEL_RETRY_COUNT: 3,
28
+ TUNNEL_RETRY_COUNT: 2, // Reduced from 3
30
29
  };
30
+ // Fast ID generator - no crypto needed for internal IDs
31
31
  let idCounter = 0;
32
+ const ID_PREFIX = Date.now().toString(36);
32
33
  function fastId() {
33
- return `${Date.now().toString(36)}-${(idCounter++).toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
34
+ return `${ID_PREFIX}-${(++idCounter).toString(36)}`;
35
+ }
36
+ // Fast path extraction - avoids URL parsing overhead
37
+ function extractPathParts(url) {
38
+ if (!url)
39
+ return [];
40
+ const qIdx = url.indexOf('?');
41
+ const path = qIdx >= 0 ? url.slice(0, qIdx) : url;
42
+ return path.split('/');
34
43
  }
35
44
  class DainTunnelServer {
36
45
  safeSend(ws, data) {
@@ -121,11 +130,9 @@ class DainTunnelServer {
121
130
  }
122
131
  setupWebSocketServer() {
123
132
  this.wss.on("connection", (ws, req) => {
124
- var _a;
125
133
  try {
126
- const url = (0, url_1.parse)(req.url || '', true);
127
- const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
128
- const isRootConnection = !url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1];
134
+ const pathParts = extractPathParts(req.url);
135
+ const isRootConnection = pathParts.length <= 1 || !pathParts[1];
129
136
  if (isRootConnection) {
130
137
  this.handleTunnelClientConnection(ws, req);
131
138
  }
@@ -170,9 +177,7 @@ class DainTunnelServer {
170
177
  ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
171
178
  }
172
179
  handleProxiedWebSocketConnection(ws, req) {
173
- var _a;
174
- const url = (0, url_1.parse)(req.url || '', true);
175
- const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
180
+ const pathParts = extractPathParts(req.url);
176
181
  if (pathParts.length < 2) {
177
182
  ws.close(1008, "Invalid tunnel ID");
178
183
  return;
@@ -299,21 +304,20 @@ class DainTunnelServer {
299
304
  }
300
305
  }
301
306
  handleResponseMessage(data) {
302
- console.log(`[Response] Received for: ${data.requestId}, status: ${data.status}`);
303
307
  const pendingRequest = this.pendingRequests.get(data.requestId);
304
308
  if (!pendingRequest)
305
309
  return;
306
- const { res, startTime, tunnelId, timeoutId } = pendingRequest;
307
- console.log(`[Response] Completed in ${Date.now() - startTime}ms`);
310
+ const { res, tunnelId, timeoutId } = pendingRequest;
311
+ this.pendingRequests.delete(data.requestId);
308
312
  if (timeoutId)
309
313
  clearTimeout(timeoutId);
310
314
  this.decrementRequestCount(tunnelId);
311
- const headers = { ...data.headers };
315
+ // Modify headers in place instead of spreading
316
+ const headers = data.headers;
312
317
  delete headers["transfer-encoding"];
313
318
  delete headers["content-length"];
314
319
  const bodyBuffer = Buffer.from(data.body, "base64");
315
320
  res.status(data.status).set(headers).set("Content-Length", bodyBuffer.length.toString()).send(bodyBuffer);
316
- this.pendingRequests.delete(data.requestId);
317
321
  }
318
322
  handleSSEMessage(data) {
319
323
  const connection = this.sseConnections.get(data.id);
@@ -340,12 +344,11 @@ class DainTunnelServer {
340
344
  return;
341
345
  }
342
346
  try {
343
- if (data.event)
344
- res.write(`event: ${data.event}\n`);
345
- for (const line of data.data.split('\n')) {
346
- res.write(`data: ${line}\n`);
347
- }
348
- res.write('\n');
347
+ // Batch write SSE event in single call
348
+ const lines = data.data.split('\n');
349
+ const message = (data.event ? `event: ${data.event}\n` : '') +
350
+ lines.map(line => `data: ${line}`).join('\n') + '\n\n';
351
+ res.write(message);
349
352
  }
350
353
  catch (_c) {
351
354
  this.cleanupSSEConnection(data.id, tunnelId);
@@ -382,10 +385,9 @@ class DainTunnelServer {
382
385
  }
383
386
  }
384
387
  async handleRequest(req, res) {
385
- var _a, _b, _c;
388
+ var _a, _b;
386
389
  const tunnelId = req.params.tunnelId;
387
- console.log(`[Request] ${req.method} /${tunnelId}${req.url}, Accept: ${(_a = req.headers.accept) === null || _a === void 0 ? void 0 : _a.substring(0, 30)}`);
388
- if (((_b = req.headers.upgrade) === null || _b === void 0 ? void 0 : _b.toLowerCase()) === 'websocket')
390
+ if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
389
391
  return;
390
392
  let tunnel;
391
393
  let retries = LIMITS.TUNNEL_RETRY_COUNT;
@@ -397,17 +399,14 @@ class DainTunnelServer {
397
399
  }
398
400
  }
399
401
  if (!tunnel) {
400
- const available = Array.from(this.tunnels.keys());
401
- console.log(`[Request] Tunnel not found: ${tunnelId}, available tunnels: [${available.join(', ')}], count: ${available.length}`);
402
402
  this.decrementRequestCount(tunnelId);
403
403
  res.status(502).json({
404
404
  error: "Bad Gateway",
405
405
  message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
406
- availableTunnels: available.length
406
+ availableTunnels: this.tunnels.size
407
407
  });
408
408
  return;
409
409
  }
410
- console.log(`[Request] Tunnel found: ${tunnelId}, wsState: ${tunnel.ws.readyState}`);
411
410
  if (tunnel.ws.bufferedAmount > LIMITS.BACKPRESSURE_THRESHOLD) {
412
411
  res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
413
412
  return;
@@ -418,7 +417,7 @@ class DainTunnelServer {
418
417
  return;
419
418
  }
420
419
  this.tunnelRequestCount.set(tunnelId, currentCount + 1);
421
- if ((_c = req.headers.accept) === null || _c === void 0 ? void 0 : _c.includes('text/event-stream')) {
420
+ if ((_b = req.headers.accept) === null || _b === void 0 ? void 0 : _b.includes('text/event-stream')) {
422
421
  this.handleSSERequest(req, res, tunnelId, tunnel);
423
422
  return;
424
423
  }
@@ -435,7 +434,6 @@ class DainTunnelServer {
435
434
  }
436
435
  }, TIMEOUTS.REQUEST_TIMEOUT);
437
436
  this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
438
- console.log(`[Request] Sending to tunnel client: ${requestId}, wsState: ${tunnel.ws.readyState}, buffered: ${tunnel.ws.bufferedAmount}`);
439
437
  const hasBody = req.method !== "GET" && req.method !== "HEAD" &&
440
438
  req.body && Buffer.isBuffer(req.body) && req.body.length > 0;
441
439
  const sent = this.safeSend(tunnel.ws, {
@@ -446,7 +444,6 @@ class DainTunnelServer {
446
444
  headers: req.headers,
447
445
  body: hasBody ? req.body.toString("base64") : undefined,
448
446
  });
449
- console.log(`[Request] Sent to tunnel client: ${sent}`);
450
447
  if (!sent) {
451
448
  clearTimeout(timeoutId);
452
449
  this.pendingRequests.delete(requestId);
@@ -459,8 +456,6 @@ class DainTunnelServer {
459
456
  handleSSERequest(req, res, tunnelId, tunnel) {
460
457
  var _a, _b;
461
458
  const sseId = fastId();
462
- const startTime = Date.now();
463
- console.log(`[SSE] ${sseId} started: ${req.url}`);
464
459
  if (req.socket) {
465
460
  req.socket.setNoDelay(true);
466
461
  req.socket.setTimeout(0);
@@ -525,7 +520,6 @@ class DainTunnelServer {
525
520
  if (cleanedUp)
526
521
  return;
527
522
  cleanedUp = true;
528
- console.log(`[SSE] ${sseId} completed in ${Date.now() - startTime}ms`);
529
523
  this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
530
524
  this.cleanupSSEConnection(sseId, tunnelId);
531
525
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.1.29",
3
+ "version": "1.1.31",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "private": false,