@dainprotocol/tunnel 1.1.30 → 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.
- package/dist/client/index.js +19 -27
- package/dist/server/index.js +29 -35
- package/package.json +11 -10
package/dist/client/index.js
CHANGED
|
@@ -31,9 +31,6 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
31
31
|
this.webSocketClients = new Map();
|
|
32
32
|
this.sseClients = new Map();
|
|
33
33
|
this.heartbeatInterval = null;
|
|
34
|
-
console.log('[DainTunnel] ========================================');
|
|
35
|
-
console.log('[DainTunnel] FIXED VERSION WITH X-FORWARDED-HOST SUPPORT');
|
|
36
|
-
console.log('[DainTunnel] ========================================');
|
|
37
34
|
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
38
35
|
if (!parsed) {
|
|
39
36
|
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
@@ -328,15 +325,12 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
328
325
|
const headers = { ...message.headers };
|
|
329
326
|
// Preserve original host for JWT audience validation
|
|
330
327
|
const originalHost = message.headers.host;
|
|
331
|
-
console.log(`[TunnelClient SSE] Original host from request: ${originalHost}`);
|
|
332
328
|
if (originalHost && !headers['x-forwarded-host']) {
|
|
333
329
|
headers['x-forwarded-host'] = originalHost;
|
|
334
330
|
headers['x-forwarded-proto'] = 'https';
|
|
335
|
-
console.log(`[TunnelClient SSE] Set x-forwarded-host: ${originalHost}`);
|
|
336
331
|
}
|
|
337
332
|
delete headers.host;
|
|
338
333
|
headers['accept-encoding'] = 'identity';
|
|
339
|
-
console.log(`[TunnelClient SSE] Forwarding to localhost:${this.port}${message.path} with x-forwarded-host: ${headers['x-forwarded-host']}`);
|
|
340
334
|
const req = http_1.default.request({
|
|
341
335
|
hostname: 'localhost',
|
|
342
336
|
port: this.port,
|
|
@@ -346,11 +340,9 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
346
340
|
agent: this.httpAgent,
|
|
347
341
|
}, (res) => {
|
|
348
342
|
if (res.statusCode !== 200) {
|
|
349
|
-
console.log(`[TunnelClient SSE] Got non-200 status: ${res.statusCode}`);
|
|
350
343
|
let errorBody = '';
|
|
351
344
|
res.on('data', (chunk) => { errorBody += chunk.toString(); });
|
|
352
345
|
res.on('end', () => {
|
|
353
|
-
console.log(`[TunnelClient SSE] Error body: ${errorBody.substring(0, 500)}`);
|
|
354
346
|
sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
|
|
355
347
|
});
|
|
356
348
|
return;
|
|
@@ -361,28 +353,31 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
361
353
|
sendSseEvent('connected');
|
|
362
354
|
let buffer = '';
|
|
363
355
|
const processBuffer = () => {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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] === ':')
|
|
370
366
|
continue;
|
|
371
367
|
let event = 'message';
|
|
372
368
|
let data = '';
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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();
|
|
376
374
|
}
|
|
377
|
-
else if (line.startsWith('data:')) {
|
|
378
|
-
data += line.
|
|
375
|
+
else if (line.length > 5 && line[0] === 'd' && line.startsWith('data:')) {
|
|
376
|
+
data += (data ? '\n' : '') + line.slice(5).trim();
|
|
379
377
|
}
|
|
380
378
|
}
|
|
381
|
-
if (data
|
|
382
|
-
data = data.slice(0, -1);
|
|
383
|
-
if (data) {
|
|
379
|
+
if (data)
|
|
384
380
|
sendSseEvent(event, data);
|
|
385
|
-
}
|
|
386
381
|
}
|
|
387
382
|
};
|
|
388
383
|
res.on('data', (chunk) => {
|
|
@@ -444,14 +439,11 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
444
439
|
// Preserve original host for JWT audience validation
|
|
445
440
|
const headers = { ...request.headers };
|
|
446
441
|
const originalHost = request.headers.host;
|
|
447
|
-
console.log(`[TunnelClient] Original host from request: ${originalHost}`);
|
|
448
442
|
if (originalHost && !headers['x-forwarded-host']) {
|
|
449
443
|
headers['x-forwarded-host'] = originalHost;
|
|
450
444
|
headers['x-forwarded-proto'] = 'https';
|
|
451
|
-
console.log(`[TunnelClient] Set x-forwarded-host: ${originalHost}`);
|
|
452
445
|
}
|
|
453
|
-
delete headers.host;
|
|
454
|
-
console.log(`[TunnelClient] Forwarding to localhost:${this.port}${request.path} with x-forwarded-host: ${headers['x-forwarded-host']}`);
|
|
446
|
+
delete headers.host;
|
|
455
447
|
const options = {
|
|
456
448
|
hostname: 'localhost',
|
|
457
449
|
port: this.port,
|
package/dist/server/index.js
CHANGED
|
@@ -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:
|
|
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 `${
|
|
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
|
|
127
|
-
const
|
|
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
|
-
|
|
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,
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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
|
|
388
|
+
var _a, _b;
|
|
386
389
|
const tunnelId = req.params.tunnelId;
|
|
387
|
-
|
|
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:
|
|
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 ((
|
|
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,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.31",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsc",
|
|
12
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
13
|
+
"test": "jest",
|
|
14
|
+
"test:watch": "jest --watch",
|
|
15
|
+
"prepublishOnly": "npm run build && npm run build:types",
|
|
16
|
+
"start": "node dist/server/start.js",
|
|
17
|
+
"start-server": "ts-node src/server/start.ts"
|
|
18
|
+
},
|
|
10
19
|
"keywords": [],
|
|
11
20
|
"author": "Ryan",
|
|
12
21
|
"license": "ISC",
|
|
@@ -68,13 +77,5 @@
|
|
|
68
77
|
"./dist/server/*.d.ts"
|
|
69
78
|
]
|
|
70
79
|
}
|
|
71
|
-
},
|
|
72
|
-
"scripts": {
|
|
73
|
-
"build": "tsc",
|
|
74
|
-
"build:types": "tsc --emitDeclarationOnly",
|
|
75
|
-
"test": "jest",
|
|
76
|
-
"test:watch": "jest --watch",
|
|
77
|
-
"start": "node dist/server/start.js",
|
|
78
|
-
"start-server": "ts-node src/server/start.ts"
|
|
79
80
|
}
|
|
80
|
-
}
|
|
81
|
+
}
|