@dainprotocol/tunnel 1.1.25 → 1.1.29

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.
@@ -10,40 +10,45 @@ 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
12
  const url_1 = require("url");
13
- // High-frequency optimization: Fast ID generation using crypto.randomBytes
14
- // ~10x faster than uuid v4 for high-frequency scenarios
13
+ const TIMEOUTS = {
14
+ CHALLENGE_TTL: 30000,
15
+ PING_INTERVAL: 30000,
16
+ REQUEST_TIMEOUT: 30000,
17
+ SSE_KEEPALIVE: 5000,
18
+ TUNNEL_RETRY_DELAY: 100,
19
+ SERVER_KEEPALIVE: 65000,
20
+ SERVER_HEADERS: 66000,
21
+ };
22
+ const LIMITS = {
23
+ MAX_CONCURRENT_REQUESTS_PER_TUNNEL: 100,
24
+ MAX_MISSED_PONGS: 2,
25
+ MAX_PAYLOAD_BYTES: 100 * 1024 * 1024,
26
+ BACKPRESSURE_THRESHOLD: 1024 * 1024,
27
+ SERVER_MAX_HEADERS: 100,
28
+ WS_BACKLOG: 100,
29
+ TUNNEL_RETRY_COUNT: 3,
30
+ };
15
31
  let idCounter = 0;
16
32
  function fastId() {
17
33
  return `${Date.now().toString(36)}-${(idCounter++).toString(36)}-${(0, crypto_1.randomBytes)(4).toString('hex')}`;
18
34
  }
19
- /**
20
- * Force immediate flush of SSE data to prevent buffering
21
- * SSE requires immediate delivery; Node.js res.write() doesn't guarantee this
22
- *
23
- * @param res Express response object
24
- */
25
- function flushSSE(res) {
26
- try {
27
- // Node.js TCP sockets use cork/uncork for buffering control
28
- // We uncork to force immediate transmission of buffered data
29
- const socket = res.socket;
30
- if (socket) {
31
- // If socket is corked, uncork it to flush immediately
32
- if (socket.uncork) {
33
- socket.uncork();
34
- }
35
- // Disable Nagle's algorithm for low-latency streaming
36
- if (socket.setNoDelay && !socket._noDelay) {
37
- socket.setNoDelay(true);
35
+ class DainTunnelServer {
36
+ safeSend(ws, data) {
37
+ try {
38
+ if (ws.readyState === ws_1.default.OPEN) {
39
+ ws.send(JSON.stringify(data));
40
+ return true;
38
41
  }
39
42
  }
43
+ catch (error) {
44
+ console.error("[Tunnel] SafeSend error:", error);
45
+ }
46
+ return false;
40
47
  }
41
- catch (error) {
42
- // Flush is best-effort; log but don't fail the request
43
- console.error('[SSE Flush] Error flushing socket:', error);
48
+ decrementRequestCount(tunnelId) {
49
+ const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
50
+ this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
44
51
  }
45
- }
46
- class DainTunnelServer {
47
52
  constructor(hostname, port) {
48
53
  this.hostname = hostname;
49
54
  this.port = port;
@@ -52,34 +57,35 @@ class DainTunnelServer {
52
57
  this.challenges = new Map();
53
58
  this.sseConnections = new Map();
54
59
  this.wsConnections = new Map();
55
- // High-frequency optimization: Track active request count per tunnel for backpressure
56
60
  this.tunnelRequestCount = new Map();
57
- this.MAX_CONCURRENT_REQUESTS_PER_TUNNEL = 100;
58
61
  this.app = (0, express_1.default)();
59
62
  this.server = http_1.default.createServer(this.app);
60
- // High-frequency optimization: Configure server for better throughput
61
- this.server.keepAliveTimeout = 65000; // Keep connections alive longer (default 5s)
62
- this.server.headersTimeout = 66000; // Must be higher than keepAliveTimeout
63
- this.server.maxHeadersCount = 100; // Limit header count for security
63
+ this.server.keepAliveTimeout = TIMEOUTS.SERVER_KEEPALIVE;
64
+ this.server.headersTimeout = TIMEOUTS.SERVER_HEADERS;
65
+ this.server.maxHeadersCount = LIMITS.SERVER_MAX_HEADERS;
64
66
  this.wss = new ws_1.default.Server({
65
67
  server: this.server,
66
- path: undefined, // Allow connections on any path
67
- // High-frequency optimization: Increase backlog and disable per-message deflate
68
- backlog: 100,
69
- perMessageDeflate: false, // Disable compression for lower latency
70
- maxPayload: 100 * 1024 * 1024, // 100MB max payload
68
+ path: undefined,
69
+ backlog: LIMITS.WS_BACKLOG,
70
+ perMessageDeflate: false,
71
+ maxPayload: LIMITS.MAX_PAYLOAD_BYTES,
71
72
  });
72
- // Single optimized CORS middleware (avoid duplicate cors() + manual headers)
73
73
  this.app.use((req, res, next) => {
74
- res.header("Access-Control-Allow-Origin", "*");
74
+ const origin = req.headers.origin;
75
+ if (origin) {
76
+ res.header("Access-Control-Allow-Origin", origin);
77
+ res.header("Vary", "Origin");
78
+ res.header("Access-Control-Allow-Credentials", "true");
79
+ }
80
+ else {
81
+ res.header("Access-Control-Allow-Origin", "*");
82
+ }
75
83
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
76
84
  res.header("Access-Control-Allow-Headers", "X-DAIN-SIGNATURE, X-DAIN-SMART-ACCOUNT-PDA, X-DAIN-AGENT-ID, X-DAIN-ORG-ID, X-DAIN-ADDRESS, X-DAIN-TIMESTAMP, X-DAIN-WEBHOOK-URL, Content-Type, Authorization, Accept, Origin, X-Requested-With");
77
- res.header("Access-Control-Allow-Credentials", "true");
78
85
  if (req.method === "OPTIONS")
79
86
  return res.sendStatus(204);
80
87
  next();
81
88
  });
82
- // Add body-parser middleware (skip for GET/HEAD/OPTIONS which don't have bodies)
83
89
  this.app.use((req, res, next) => {
84
90
  if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
85
91
  return next();
@@ -90,18 +96,37 @@ class DainTunnelServer {
90
96
  this.setupWebSocketServer();
91
97
  }
92
98
  setupExpressRoutes() {
93
- // Generic route handler for all tunnel requests
99
+ this.app.get("/", (req, res, next) => {
100
+ var _a;
101
+ if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
102
+ return next();
103
+ res.status(200).json({
104
+ status: "healthy",
105
+ tunnels: this.tunnels.size,
106
+ pendingRequests: this.pendingRequests.size,
107
+ sseConnections: this.sseConnections.size,
108
+ wsConnections: this.wsConnections.size,
109
+ uptime: process.uptime()
110
+ });
111
+ });
112
+ this.app.get("/health", (req, res) => {
113
+ res.status(200).json({
114
+ status: "ok",
115
+ tunnels: this.tunnels.size,
116
+ pendingRequests: this.pendingRequests.size,
117
+ uptime: process.uptime()
118
+ });
119
+ });
94
120
  this.app.use("/:tunnelId", this.handleRequest.bind(this));
95
121
  }
96
122
  setupWebSocketServer() {
97
- // Handle WebSocket connections from tunnel clients
98
123
  this.wss.on("connection", (ws, req) => {
99
124
  var _a;
100
125
  try {
101
126
  const url = (0, url_1.parse)(req.url || '', true);
102
127
  const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
103
- // Client tunnel connection has no tunnelId in the path (or is at root)
104
- if (!url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1]) {
128
+ const isRootConnection = !url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1];
129
+ if (isRootConnection) {
105
130
  this.handleTunnelClientConnection(ws, req);
106
131
  }
107
132
  else {
@@ -114,25 +139,26 @@ class DainTunnelServer {
114
139
  }
115
140
  });
116
141
  }
117
- // Handle WebSocket connections from tunnel clients
118
142
  handleTunnelClientConnection(ws, req) {
119
143
  ws.on("message", (message) => {
120
144
  try {
121
145
  const data = JSON.parse(message);
122
- if (data.type === "challenge_request") {
123
- this.handleChallengeRequest(ws);
124
- }
125
- else if (data.type === "start") {
126
- this.handleStartMessage(ws, data);
127
- }
128
- else if (data.type === "response") {
129
- this.handleResponseMessage(data);
130
- }
131
- else if (data.type === "sse") {
132
- this.handleSSEMessage(data);
133
- }
134
- else if (data.type === "websocket") {
135
- this.handleWebSocketMessage(data);
146
+ switch (data.type) {
147
+ case "challenge_request":
148
+ this.handleChallengeRequest(ws);
149
+ break;
150
+ case "start":
151
+ this.handleStartMessage(ws, data);
152
+ break;
153
+ case "response":
154
+ this.handleResponseMessage(data);
155
+ break;
156
+ case "sse":
157
+ this.handleSSEMessage(data);
158
+ break;
159
+ case "websocket":
160
+ this.handleWebSocketMessage(data);
161
+ break;
136
162
  }
137
163
  }
138
164
  catch (error) {
@@ -143,7 +169,6 @@ class DainTunnelServer {
143
169
  ws.on("close", () => this.removeTunnel(ws));
144
170
  ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
145
171
  }
146
- // Handle incoming WebSocket connections to be proxied through the tunnel
147
172
  handleProxiedWebSocketConnection(ws, req) {
148
173
  var _a;
149
174
  const url = (0, url_1.parse)(req.url || '', true);
@@ -159,57 +184,35 @@ class DainTunnelServer {
159
184
  ws.close(1008, "Tunnel not found");
160
185
  return;
161
186
  }
162
- // Create a WebSocket connection ID
163
187
  const wsConnectionId = fastId();
164
- // Store this connection
165
188
  this.wsConnections.set(wsConnectionId, {
166
189
  clientSocket: ws,
167
190
  id: wsConnectionId,
168
191
  path: remainingPath,
169
- headers: req.headers
192
+ headers: req.headers,
193
+ tunnelId
170
194
  });
171
- // Notify tunnel client about the new WebSocket connection
172
- tunnel.ws.send(JSON.stringify({
195
+ this.safeSend(tunnel.ws, {
173
196
  type: "websocket_connection",
174
197
  id: wsConnectionId,
175
198
  path: remainingPath,
176
199
  headers: req.headers
177
- }));
178
- // Handle messages from the client
179
- ws.on("message", (data) => {
180
- tunnel.ws.send(JSON.stringify({
181
- type: "websocket",
182
- id: wsConnectionId,
183
- event: "message",
184
- data: data.toString('base64')
185
- }));
186
- });
187
- // Handle client disconnection
188
- ws.on("close", () => {
189
- tunnel.ws.send(JSON.stringify({
190
- type: "websocket",
191
- id: wsConnectionId,
192
- event: "close"
193
- }));
194
- this.wsConnections.delete(wsConnectionId);
195
- });
196
- // Handle errors
197
- ws.on("error", (error) => {
198
- tunnel.ws.send(JSON.stringify({
199
- type: "websocket",
200
- id: wsConnectionId,
201
- event: "error",
202
- data: error.message
203
- }));
204
200
  });
201
+ const sendToTunnel = (event, data) => {
202
+ const currentTunnel = this.tunnels.get(tunnelId);
203
+ if (currentTunnel) {
204
+ this.safeSend(currentTunnel.ws, { type: "websocket", id: wsConnectionId, event, data });
205
+ }
206
+ };
207
+ ws.on("message", (data) => sendToTunnel("message", data.toString('base64')));
208
+ ws.on("close", () => { sendToTunnel("close"); this.wsConnections.delete(wsConnectionId); });
209
+ ws.on("error", (error) => sendToTunnel("error", error.message));
205
210
  }
206
211
  handleChallengeRequest(ws) {
207
212
  const challenge = fastId();
208
- const challengeObj = { ws, challenge, timestamp: Date.now() };
209
- this.challenges.set(challenge, challengeObj);
213
+ this.challenges.set(challenge, { ws, challenge, timestamp: Date.now() });
210
214
  ws.send(JSON.stringify({ type: "challenge", challenge }));
211
- // Auto-cleanup expired challenges (30 second TTL)
212
- setTimeout(() => this.challenges.delete(challenge), 30000);
215
+ setTimeout(() => this.challenges.delete(challenge), TIMEOUTS.CHALLENGE_TTL);
213
216
  }
214
217
  handleStartMessage(ws, data) {
215
218
  const { challenge, signature, tunnelId, apiKey } = data;
@@ -220,7 +223,6 @@ class DainTunnelServer {
220
223
  }
221
224
  this.challenges.delete(challenge);
222
225
  try {
223
- // Parse API key to get the secret for HMAC validation
224
226
  if (!apiKey) {
225
227
  ws.close(1008, "API key required");
226
228
  return;
@@ -230,65 +232,63 @@ class DainTunnelServer {
230
232
  ws.close(1008, "Invalid API key format");
231
233
  return;
232
234
  }
233
- // Validate HMAC signature using constant-time comparison
234
- const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret)
235
- .update(challenge)
236
- .digest('hex');
237
- // Convert to buffers for timing-safe comparison
235
+ const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret).update(challenge).digest('hex');
238
236
  const expectedSigBuffer = Buffer.from(expectedSignature, 'hex');
239
237
  const receivedSigBuffer = Buffer.from(signature, 'hex');
240
- // Constant-time comparison to prevent timing attacks
241
238
  if (expectedSigBuffer.length !== receivedSigBuffer.length ||
242
239
  !(0, crypto_1.timingSafeEqual)(expectedSigBuffer, receivedSigBuffer)) {
243
240
  ws.close(1008, "Invalid signature");
244
241
  return;
245
242
  }
246
- // Verify that tunnelId matches orgId_agentId from the API key
247
243
  const expectedTunnelId = `${parsed.orgId}_${parsed.agentId}`;
248
244
  if (tunnelId !== expectedTunnelId) {
249
245
  ws.close(1008, `Tunnel ID does not match API key. Expected: ${expectedTunnelId}, Got: ${tunnelId}`);
250
246
  return;
251
247
  }
252
- // If tunnel already exists, remove old one
253
- if (this.tunnels.has(tunnelId)) {
254
- const oldTunnel = this.tunnels.get(tunnelId);
255
- if (oldTunnel && oldTunnel.ws !== ws) {
256
- oldTunnel.ws.close(1000, "Replaced by new connection");
257
- }
248
+ const existingTunnel = this.tunnels.get(tunnelId);
249
+ if (existingTunnel && existingTunnel.ws !== ws) {
250
+ existingTunnel.ws.close(1000, "Replaced by new connection");
258
251
  }
259
252
  this.tunnels.set(tunnelId, { id: tunnelId, ws });
260
- // Save tunnel ID on the WebSocket object for easy lookup on close
261
253
  ws.tunnelId = tunnelId;
262
- // Add a periodic ping to keep the connection alive (silent - no logging)
254
+ let isAlive = true;
255
+ let missedPongs = 0;
256
+ ws.on("pong", () => { isAlive = true; missedPongs = 0; });
263
257
  const intervalId = setInterval(() => {
264
- if (this.tunnels.has(tunnelId)) {
265
- const tunnel = this.tunnels.get(tunnelId);
266
- if (tunnel && tunnel.ws === ws && ws.readyState === ws_1.default.OPEN) {
267
- try {
268
- ws.ping();
269
- }
270
- catch (error) {
271
- clearInterval(intervalId);
272
- }
273
- }
274
- else {
258
+ if (!this.tunnels.has(tunnelId)) {
259
+ clearInterval(intervalId);
260
+ return;
261
+ }
262
+ const tunnel = this.tunnels.get(tunnelId);
263
+ if (!tunnel || tunnel.ws !== ws || ws.readyState !== ws_1.default.OPEN) {
264
+ clearInterval(intervalId);
265
+ if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws)
266
+ this.tunnels.delete(tunnelId);
267
+ return;
268
+ }
269
+ if (!isAlive) {
270
+ missedPongs++;
271
+ if (missedPongs >= LIMITS.MAX_MISSED_PONGS) {
272
+ console.log(`[Tunnel] ${tunnelId} failed liveness check (${missedPongs} missed pongs), terminating`);
275
273
  clearInterval(intervalId);
276
- if (tunnel && tunnel.ws === ws) {
277
- this.tunnels.delete(tunnelId);
278
- }
274
+ ws.terminate();
275
+ return;
279
276
  }
280
277
  }
281
- else {
278
+ isAlive = false;
279
+ try {
280
+ ws.ping();
281
+ }
282
+ catch (_a) {
282
283
  clearInterval(intervalId);
284
+ ws.terminate();
283
285
  }
284
- }, 30000); // Ping every 30 seconds (reduced frequency)
285
- // Store the interval ID on the WebSocket object so we can clean it up
286
+ }, TIMEOUTS.PING_INTERVAL);
286
287
  ws.keepAliveInterval = intervalId;
287
288
  ws.on("close", () => clearInterval(intervalId));
288
- let tunnelUrl = `${this.hostname}`;
289
- if (process.env.SKIP_PORT !== "true") {
289
+ let tunnelUrl = this.hostname;
290
+ if (process.env.SKIP_PORT !== "true")
290
291
  tunnelUrl += `:${this.port}`;
291
- }
292
292
  tunnelUrl += `/${tunnelId}`;
293
293
  ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
294
294
  console.log(`[Tunnel] Created: ${tunnelUrl}`);
@@ -299,84 +299,63 @@ class DainTunnelServer {
299
299
  }
300
300
  }
301
301
  handleResponseMessage(data) {
302
+ console.log(`[Response] Received for: ${data.requestId}, status: ${data.status}`);
302
303
  const pendingRequest = this.pendingRequests.get(data.requestId);
303
- if (pendingRequest) {
304
- const { res, startTime, tunnelId, timeoutId } = pendingRequest;
305
- const endTime = Date.now();
306
- // Clear the timeout since we received a response
307
- if (timeoutId) {
308
- clearTimeout(timeoutId);
309
- }
310
- // Decrement request counter for backpressure tracking
311
- const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
312
- this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
313
- const headers = { ...data.headers };
314
- delete headers["transfer-encoding"];
315
- delete headers["content-length"];
316
- const bodyBuffer = Buffer.from(data.body, "base64");
317
- res
318
- .status(data.status)
319
- .set(headers)
320
- .set("Content-Length", bodyBuffer.length.toString())
321
- .send(bodyBuffer);
322
- this.pendingRequests.delete(data.requestId);
323
- }
304
+ if (!pendingRequest)
305
+ return;
306
+ const { res, startTime, tunnelId, timeoutId } = pendingRequest;
307
+ console.log(`[Response] Completed in ${Date.now() - startTime}ms`);
308
+ if (timeoutId)
309
+ clearTimeout(timeoutId);
310
+ this.decrementRequestCount(tunnelId);
311
+ const headers = { ...data.headers };
312
+ delete headers["transfer-encoding"];
313
+ delete headers["content-length"];
314
+ const bodyBuffer = Buffer.from(data.body, "base64");
315
+ res.status(data.status).set(headers).set("Content-Length", bodyBuffer.length.toString()).send(bodyBuffer);
316
+ this.pendingRequests.delete(data.requestId);
324
317
  }
325
318
  handleSSEMessage(data) {
326
319
  const connection = this.sseConnections.get(data.id);
327
320
  if (!connection)
328
321
  return;
329
322
  const { res, tunnelId } = connection;
330
- const conn = connection;
331
- // Skip 'connected' event - we already wrote headers
332
323
  if (data.event === 'connected')
333
324
  return;
334
- // Handle close event
335
325
  if (data.event === 'close') {
336
- const tunnel = this.tunnels.get(tunnelId);
337
- if (tunnel)
338
- tunnel.ws.send(JSON.stringify({ type: "sse_close", id: data.id }));
339
- if (!conn.clientDisconnected && res.writable) {
340
- try {
341
- res.end();
342
- }
343
- catch (_a) { }
326
+ try {
327
+ res.end();
344
328
  }
329
+ catch (_a) { }
345
330
  this.cleanupSSEConnection(data.id, tunnelId);
346
331
  return;
347
332
  }
348
- // Handle error - send as SSE error event (headers already written)
349
333
  if (data.event === 'error') {
350
- if (!conn.clientDisconnected && res.writable) {
351
- try {
352
- res.write(`event: error\ndata: ${data.data}\n\n`);
353
- flushSSE(res);
354
- res.end();
355
- }
356
- catch (_b) { }
334
+ try {
335
+ res.write(`event: error\ndata: ${data.data}\n\n`);
336
+ res.end();
357
337
  }
338
+ catch (_b) { }
358
339
  this.cleanupSSEConnection(data.id, tunnelId);
359
340
  return;
360
341
  }
361
- // Skip if client disconnected
362
- if (conn.clientDisconnected || !res.writable) {
363
- conn.clientDisconnected = true;
364
- return;
365
- }
366
- // Forward SSE event
367
342
  try {
368
343
  if (data.event)
369
344
  res.write(`event: ${data.event}\n`);
370
- res.write(`data: ${data.data.replace(/\n/g, '\ndata: ')}\n\n`);
371
- flushSSE(res);
345
+ for (const line of data.data.split('\n')) {
346
+ res.write(`data: ${line}\n`);
347
+ }
348
+ res.write('\n');
372
349
  }
373
350
  catch (_c) {
374
- conn.clientDisconnected = true;
351
+ this.cleanupSSEConnection(data.id, tunnelId);
375
352
  }
376
353
  }
377
354
  cleanupSSEConnection(id, tunnelId) {
378
- const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
379
- this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
355
+ const connection = this.sseConnections.get(id);
356
+ if (connection === null || connection === void 0 ? void 0 : connection.keepAliveInterval)
357
+ clearInterval(connection.keepAliveInterval);
358
+ this.decrementRequestCount(tunnelId);
380
359
  this.sseConnections.delete(id);
381
360
  }
382
361
  handleWebSocketMessage(data) {
@@ -384,199 +363,260 @@ class DainTunnelServer {
384
363
  if (!connection)
385
364
  return;
386
365
  const { clientSocket } = connection;
387
- if (data.event === 'message' && data.data) {
388
- const messageData = Buffer.from(data.data, 'base64');
389
- if (clientSocket.readyState === ws_1.default.OPEN) {
390
- clientSocket.send(messageData);
391
- }
392
- }
393
- else if (data.event === 'close') {
394
- if (clientSocket.readyState === ws_1.default.OPEN) {
395
- clientSocket.close();
396
- }
397
- this.wsConnections.delete(data.id);
398
- }
399
- else if (data.event === 'error' && data.data) {
400
- if (clientSocket.readyState === ws_1.default.OPEN) {
401
- clientSocket.close(1011, data.data);
402
- }
403
- this.wsConnections.delete(data.id);
366
+ const isOpen = clientSocket.readyState === ws_1.default.OPEN;
367
+ switch (data.event) {
368
+ case 'message':
369
+ if (data.data && isOpen)
370
+ clientSocket.send(Buffer.from(data.data, 'base64'));
371
+ break;
372
+ case 'close':
373
+ if (isOpen)
374
+ clientSocket.close();
375
+ this.wsConnections.delete(data.id);
376
+ break;
377
+ case 'error':
378
+ if (isOpen)
379
+ clientSocket.close(1011, data.data);
380
+ this.wsConnections.delete(data.id);
381
+ break;
404
382
  }
405
383
  }
406
384
  async handleRequest(req, res) {
385
+ var _a, _b, _c;
407
386
  const tunnelId = req.params.tunnelId;
408
- // Check for upgraded connections (WebSockets) - these are handled by the WebSocket server
409
- if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
410
- // This is handled by the WebSocket server now
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')
411
389
  return;
412
- }
413
390
  let tunnel;
414
- let retries = 3;
391
+ let retries = LIMITS.TUNNEL_RETRY_COUNT;
415
392
  while (retries > 0 && !tunnel) {
416
393
  tunnel = this.tunnels.get(tunnelId);
417
394
  if (!tunnel) {
418
- await new Promise(resolve => setTimeout(resolve, 100));
395
+ await new Promise(resolve => setTimeout(resolve, TIMEOUTS.TUNNEL_RETRY_DELAY));
419
396
  retries--;
420
397
  }
421
398
  }
422
399
  if (!tunnel) {
423
- return res.status(404).send("Tunnel not found");
400
+ const available = Array.from(this.tunnels.keys());
401
+ console.log(`[Request] Tunnel not found: ${tunnelId}, available tunnels: [${available.join(', ')}], count: ${available.length}`);
402
+ this.decrementRequestCount(tunnelId);
403
+ res.status(502).json({
404
+ error: "Bad Gateway",
405
+ message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
406
+ availableTunnels: available.length
407
+ });
408
+ return;
424
409
  }
425
- // High-frequency optimization: Check WebSocket buffer and apply backpressure
426
- if (tunnel.ws.bufferedAmount > 1024 * 1024) {
427
- return res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
410
+ console.log(`[Request] Tunnel found: ${tunnelId}, wsState: ${tunnel.ws.readyState}`);
411
+ if (tunnel.ws.bufferedAmount > LIMITS.BACKPRESSURE_THRESHOLD) {
412
+ res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
413
+ return;
428
414
  }
429
- // Check concurrent request limit per tunnel
430
415
  const currentCount = this.tunnelRequestCount.get(tunnelId) || 0;
431
- if (currentCount >= this.MAX_CONCURRENT_REQUESTS_PER_TUNNEL) {
432
- return res.status(503).json({ error: "Service Unavailable", message: "Too many concurrent requests" });
416
+ if (currentCount >= LIMITS.MAX_CONCURRENT_REQUESTS_PER_TUNNEL) {
417
+ res.status(503).json({ error: "Service Unavailable", message: "Too many concurrent requests" });
418
+ return;
433
419
  }
434
420
  this.tunnelRequestCount.set(tunnelId, currentCount + 1);
435
- // Check for SSE request
436
- if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
437
- return this.handleSSERequest(req, res, tunnelId, tunnel);
421
+ if ((_c = req.headers.accept) === null || _c === void 0 ? void 0 : _c.includes('text/event-stream')) {
422
+ this.handleSSERequest(req, res, tunnelId, tunnel);
423
+ return;
438
424
  }
439
- // Handle regular HTTP request
440
425
  const requestId = fastId();
441
426
  const startTime = Date.now();
442
- // Set a timeout for the request (30 seconds)
443
- const REQUEST_TIMEOUT = 30000;
444
427
  const timeoutId = setTimeout(() => {
445
428
  const pendingRequest = this.pendingRequests.get(requestId);
446
429
  if (pendingRequest) {
447
- const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
448
- this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
430
+ this.decrementRequestCount(tunnelId);
449
431
  this.pendingRequests.delete(requestId);
450
432
  if (!res.headersSent) {
451
433
  res.status(504).json({ error: "Gateway Timeout", message: "Request timed out" });
452
434
  }
453
435
  }
454
- }, REQUEST_TIMEOUT);
436
+ }, TIMEOUTS.REQUEST_TIMEOUT);
455
437
  this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
456
- tunnel.ws.send(JSON.stringify({
438
+ console.log(`[Request] Sending to tunnel client: ${requestId}, wsState: ${tunnel.ws.readyState}, buffered: ${tunnel.ws.bufferedAmount}`);
439
+ const hasBody = req.method !== "GET" && req.method !== "HEAD" &&
440
+ req.body && Buffer.isBuffer(req.body) && req.body.length > 0;
441
+ const sent = this.safeSend(tunnel.ws, {
457
442
  type: "request",
458
443
  id: requestId,
459
444
  method: req.method,
460
445
  path: req.url,
461
446
  headers: req.headers,
462
- body: req.method !== "GET" && req.method !== "HEAD" && req.body
463
- ? req.body.toString("base64")
464
- : undefined,
465
- }));
447
+ body: hasBody ? req.body.toString("base64") : undefined,
448
+ });
449
+ console.log(`[Request] Sent to tunnel client: ${sent}`);
450
+ if (!sent) {
451
+ clearTimeout(timeoutId);
452
+ this.pendingRequests.delete(requestId);
453
+ this.decrementRequestCount(tunnelId);
454
+ if (!res.headersSent) {
455
+ res.status(502).json({ error: "Bad Gateway", message: "Tunnel connection lost" });
456
+ }
457
+ }
466
458
  }
467
459
  handleSSERequest(req, res, tunnelId, tunnel) {
460
+ var _a, _b;
468
461
  const sseId = fastId();
469
- // Optimize TCP for streaming
470
- const socket = req.socket || req.connection;
471
- if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
472
- socket.setNoDelay(true);
473
- // Write SSE headers immediately to keep browser connection alive
474
- res.writeHead(200, {
462
+ const startTime = Date.now();
463
+ console.log(`[SSE] ${sseId} started: ${req.url}`);
464
+ if (req.socket) {
465
+ req.socket.setNoDelay(true);
466
+ req.socket.setTimeout(0);
467
+ }
468
+ const origin = req.headers.origin;
469
+ const sseHeaders = {
475
470
  'Content-Type': 'text/event-stream',
476
- 'Cache-Control': 'no-cache',
471
+ 'Cache-Control': 'no-cache, no-transform',
472
+ 'Connection': 'keep-alive',
477
473
  'X-Accel-Buffering': 'no',
478
- });
479
- res.flushHeaders();
480
- res.write(': connected\n\n');
481
- flushSSE(res);
482
- this.sseConnections.set(sseId, {
483
- req, res, id: sseId, tunnelId,
484
- clientDisconnected: false
485
- });
486
- // Forward to tunnel client
487
- tunnel.ws.send(JSON.stringify({
474
+ 'X-Content-Type-Options': 'nosniff',
475
+ 'Content-Encoding': 'identity',
476
+ 'Transfer-Encoding': 'chunked',
477
+ };
478
+ if (typeof origin === 'string') {
479
+ sseHeaders['Access-Control-Allow-Origin'] = origin;
480
+ sseHeaders['Access-Control-Allow-Credentials'] = 'true';
481
+ sseHeaders['Vary'] = 'Origin';
482
+ }
483
+ else {
484
+ sseHeaders['Access-Control-Allow-Origin'] = '*';
485
+ }
486
+ res.writeHead(200, sseHeaders);
487
+ const flush = () => {
488
+ if (typeof res.flush === 'function')
489
+ res.flush();
490
+ };
491
+ (_a = res.flushHeaders) === null || _a === void 0 ? void 0 : _a.call(res);
492
+ res.write(':keepalive\n\n');
493
+ flush();
494
+ const keepAliveInterval = setInterval(() => {
495
+ try {
496
+ if (!res.writableEnded) {
497
+ res.write(':keepalive\n\n');
498
+ flush();
499
+ }
500
+ else
501
+ clearInterval(keepAliveInterval);
502
+ }
503
+ catch (_a) {
504
+ clearInterval(keepAliveInterval);
505
+ }
506
+ }, TIMEOUTS.SSE_KEEPALIVE);
507
+ this.sseConnections.set(sseId, { req, res, id: sseId, tunnelId, keepAliveInterval });
508
+ const hasBody = req.method !== "GET" && req.body &&
509
+ Buffer.isBuffer(req.body) && req.body.length > 0;
510
+ const sent = this.safeSend(tunnel.ws, {
488
511
  type: "sse_connection",
489
512
  id: sseId,
490
513
  path: req.url,
491
514
  method: req.method,
492
515
  headers: req.headers,
493
- body: req.method !== "GET" && req.body ? req.body.toString("base64") : undefined
494
- }));
495
- // Track response close (not request close) - for POST SSE, req closes immediately
496
- res.on('close', () => {
497
- const connection = this.sseConnections.get(sseId);
498
- if (connection) {
499
- connection.clientDisconnected = true;
500
- setTimeout(() => {
501
- if (this.sseConnections.has(sseId)) {
502
- this.cleanupSSEConnection(sseId, tunnelId);
503
- const t = this.tunnels.get(tunnelId);
504
- if ((t === null || t === void 0 ? void 0 : t.ws.readyState) === ws_1.default.OPEN) {
505
- t.ws.send(JSON.stringify({ type: "sse_close", id: sseId }));
506
- }
507
- }
508
- }, 100);
509
- }
516
+ body: hasBody ? req.body.toString("base64") : undefined
517
+ });
518
+ if (!sent) {
519
+ this.cleanupSSEConnection(sseId, tunnelId);
520
+ res.end();
521
+ return;
522
+ }
523
+ let cleanedUp = false;
524
+ const doCleanup = () => {
525
+ if (cleanedUp)
526
+ return;
527
+ cleanedUp = true;
528
+ console.log(`[SSE] ${sseId} completed in ${Date.now() - startTime}ms`);
529
+ this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
530
+ this.cleanupSSEConnection(sseId, tunnelId);
531
+ };
532
+ req.on('close', () => {
533
+ var _a;
534
+ if (((_a = req.socket) === null || _a === void 0 ? void 0 : _a.destroyed) || res.writableEnded)
535
+ doCleanup();
510
536
  });
537
+ (_b = req.socket) === null || _b === void 0 ? void 0 : _b.on('close', doCleanup);
538
+ req.on('error', doCleanup);
539
+ res.on('error', doCleanup);
511
540
  }
512
541
  removeTunnel(ws) {
513
542
  try {
514
543
  if (ws.keepAliveInterval)
515
544
  clearInterval(ws.keepAliveInterval);
516
- const tunnelId = ws.tunnelId;
517
- let removedTunnelId = tunnelId;
518
- if (tunnelId && this.tunnels.has(tunnelId)) {
519
- const tunnel = this.tunnels.get(tunnelId);
520
- if (tunnel && tunnel.ws === ws)
521
- this.tunnels.delete(tunnelId);
522
- }
523
- else {
524
- removedTunnelId = undefined;
525
- for (const [id, tunnel] of this.tunnels.entries()) {
526
- if (tunnel.ws === ws) {
527
- this.tunnels.delete(id);
528
- removedTunnelId = id;
529
- break;
530
- }
531
- }
532
- }
545
+ const removedTunnelId = this.findAndRemoveTunnel(ws);
533
546
  if (removedTunnelId) {
534
547
  this.tunnelRequestCount.delete(removedTunnelId);
535
- // Close all pending HTTP requests associated with this tunnel
536
- for (const [requestId, pendingRequest] of this.pendingRequests.entries()) {
537
- if (pendingRequest.tunnelId === removedTunnelId) {
538
- try {
539
- if (pendingRequest.timeoutId)
540
- clearTimeout(pendingRequest.timeoutId);
541
- if (!pendingRequest.res.headersSent) {
542
- pendingRequest.res.status(502).json({ error: "Bad Gateway", message: "Tunnel closed" });
543
- }
544
- }
545
- catch (error) { /* ignore */ }
546
- this.pendingRequests.delete(requestId);
547
- }
548
- }
549
- // Close all SSE connections associated with this tunnel
550
- for (const [sseId, sseConnection] of this.sseConnections.entries()) {
551
- if (sseConnection.tunnelId === removedTunnelId) {
552
- try {
553
- sseConnection.res.end();
554
- }
555
- catch (error) { /* ignore */ }
556
- this.sseConnections.delete(sseId);
548
+ this.cleanupPendingRequests(removedTunnelId);
549
+ this.cleanupTunnelSSEConnections(removedTunnelId);
550
+ this.cleanupTunnelWSConnections(removedTunnelId);
551
+ }
552
+ this.cleanupChallengesForSocket(ws);
553
+ }
554
+ catch (error) {
555
+ console.error("[Tunnel] Remove error:", error);
556
+ }
557
+ }
558
+ findAndRemoveTunnel(ws) {
559
+ const tunnelId = ws.tunnelId;
560
+ if (tunnelId && this.tunnels.has(tunnelId)) {
561
+ const tunnel = this.tunnels.get(tunnelId);
562
+ if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws) {
563
+ this.tunnels.delete(tunnelId);
564
+ return tunnelId;
565
+ }
566
+ }
567
+ for (const [id, tunnel] of this.tunnels.entries()) {
568
+ if (tunnel.ws === ws) {
569
+ this.tunnels.delete(id);
570
+ return id;
571
+ }
572
+ }
573
+ return undefined;
574
+ }
575
+ cleanupPendingRequests(tunnelId) {
576
+ for (const [requestId, pending] of this.pendingRequests.entries()) {
577
+ if (pending.tunnelId === tunnelId) {
578
+ try {
579
+ if (pending.timeoutId)
580
+ clearTimeout(pending.timeoutId);
581
+ if (!pending.res.headersSent) {
582
+ pending.res.status(502).json({ error: "Bad Gateway", message: "Tunnel closed" });
557
583
  }
558
584
  }
559
- // Close all WebSocket connections associated with this tunnel
560
- for (const [wsId, wsConnection] of this.wsConnections.entries()) {
561
- if (wsConnection.path.startsWith(`/${removedTunnelId}/`)) {
562
- try {
563
- wsConnection.clientSocket.close(1001, "Tunnel closed");
564
- }
565
- catch (error) { /* ignore */ }
566
- this.wsConnections.delete(wsId);
567
- }
585
+ catch (_a) { }
586
+ this.pendingRequests.delete(requestId);
587
+ }
588
+ }
589
+ }
590
+ cleanupTunnelSSEConnections(tunnelId) {
591
+ for (const [sseId, conn] of this.sseConnections.entries()) {
592
+ if (conn.tunnelId === tunnelId) {
593
+ if (conn.keepAliveInterval)
594
+ clearInterval(conn.keepAliveInterval);
595
+ try {
596
+ conn.res.end();
568
597
  }
598
+ catch (_a) { }
599
+ this.sseConnections.delete(sseId);
569
600
  }
570
- // Remove any pending challenges for this WebSocket
571
- for (const [challenge, challengeObj] of this.challenges.entries()) {
572
- if (challengeObj.ws === ws) {
573
- this.challenges.delete(challenge);
574
- break;
601
+ }
602
+ }
603
+ cleanupTunnelWSConnections(tunnelId) {
604
+ for (const [wsId, conn] of this.wsConnections.entries()) {
605
+ if (conn.tunnelId === tunnelId) {
606
+ try {
607
+ conn.clientSocket.close(1001, "Tunnel closed");
575
608
  }
609
+ catch (_a) { }
610
+ this.wsConnections.delete(wsId);
576
611
  }
577
612
  }
578
- catch (error) {
579
- console.error("[Tunnel] Remove error:", error);
613
+ }
614
+ cleanupChallengesForSocket(ws) {
615
+ for (const [challenge, obj] of this.challenges.entries()) {
616
+ if (obj.ws === ws) {
617
+ this.challenges.delete(challenge);
618
+ break;
619
+ }
580
620
  }
581
621
  }
582
622
  async start() {
@@ -590,37 +630,31 @@ class DainTunnelServer {
590
630
  async stop() {
591
631
  return new Promise((resolve) => {
592
632
  try {
593
- // Close all SSE connections
594
- for (const [sseId, sseConnection] of this.sseConnections.entries()) {
633
+ for (const [sseId, conn] of this.sseConnections.entries()) {
595
634
  try {
596
- sseConnection.res.end();
635
+ conn.res.end();
597
636
  }
598
637
  catch (error) {
599
- console.error(`Error closing SSE connection ${sseId}:`, error);
638
+ console.error(`Error closing SSE ${sseId}:`, error);
600
639
  }
601
640
  this.sseConnections.delete(sseId);
602
641
  }
603
- // Close all WebSocket connections
604
- for (const [wsId, wsConnection] of this.wsConnections.entries()) {
642
+ for (const [wsId, conn] of this.wsConnections.entries()) {
605
643
  try {
606
- wsConnection.clientSocket.close(1001, "Server shutting down");
644
+ conn.clientSocket.close(1001, "Server shutting down");
607
645
  }
608
646
  catch (error) {
609
- console.error(`Error closing WebSocket connection ${wsId}:`, error);
647
+ console.error(`Error closing WS ${wsId}:`, error);
610
648
  }
611
649
  this.wsConnections.delete(wsId);
612
650
  }
613
- // Close the WebSocket server
614
651
  this.wss.close(() => {
615
- // Close the HTTP server
616
- this.server.close(() => {
617
- resolve();
618
- });
652
+ this.server.close(() => resolve());
619
653
  });
620
654
  }
621
655
  catch (error) {
622
656
  console.error('Error during server shutdown:', error);
623
- resolve(); // Resolve anyway to prevent hanging
657
+ resolve();
624
658
  }
625
659
  });
626
660
  }