@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.
- package/dist/client/index.js +42 -17
- package/dist/server/index.js +29 -35
- package/package.json +1 -1
package/dist/client/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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.
|
|
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
|
|
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
|
|
452
|
+
headers,
|
|
428
453
|
agent: this.httpAgent,
|
|
429
454
|
timeout: TIMEOUTS.REQUEST,
|
|
430
455
|
};
|
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
|
};
|