@dainprotocol/tunnel 1.1.35 → 2.0.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.
@@ -1,20 +1,12 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- const express_1 = __importDefault(require("express"));
7
- const http_1 = __importDefault(require("http"));
8
- const ws_1 = __importDefault(require("ws"));
9
- const body_parser_1 = __importDefault(require("body-parser"));
10
- const auth_1 = require("@dainprotocol/service-sdk/service/auth");
11
- const crypto_1 = require("crypto");
1
+ import { Hono } from "hono";
2
+ import { parseAPIKey } from "@dainprotocol/service-sdk/service/auth";
3
+ import { createHmac, timingSafeEqual } from "crypto";
12
4
  const TIMEOUTS = {
13
5
  CHALLENGE_TTL: 30000,
14
6
  PING_INTERVAL: 30000,
15
7
  REQUEST_TIMEOUT: 30000,
16
8
  SSE_KEEPALIVE: 5000,
17
- TUNNEL_RETRY_DELAY: 20, // Reduced from 100ms
9
+ TUNNEL_RETRY_DELAY: 20,
18
10
  SERVER_KEEPALIVE: 65000,
19
11
  SERVER_HEADERS: 66000,
20
12
  };
@@ -25,15 +17,13 @@ const LIMITS = {
25
17
  BACKPRESSURE_THRESHOLD: 1024 * 1024,
26
18
  SERVER_MAX_HEADERS: 100,
27
19
  WS_BACKLOG: 100,
28
- TUNNEL_RETRY_COUNT: 2, // Reduced from 3
20
+ TUNNEL_RETRY_COUNT: 2,
29
21
  };
30
- // Fast ID generator - no crypto needed for internal IDs
31
22
  let idCounter = 0;
32
23
  const ID_PREFIX = Date.now().toString(36);
33
24
  function fastId() {
34
25
  return `${ID_PREFIX}-${(++idCounter).toString(36)}`;
35
26
  }
36
- // Fast path extraction - avoids URL parsing overhead
37
27
  function extractPathParts(url) {
38
28
  if (!url)
39
29
  return [];
@@ -41,31 +31,30 @@ function extractPathParts(url) {
41
31
  const path = qIdx >= 0 ? url.slice(0, qIdx) : url;
42
32
  return path.split('/');
43
33
  }
44
- function rawDataToString(message) {
45
- if (typeof message === "string")
46
- return message;
47
- if (Buffer.isBuffer(message))
48
- return message.toString("utf8");
49
- if (Array.isArray(message))
50
- return Buffer.concat(message).toString("utf8");
51
- return Buffer.from(message).toString("utf8");
52
- }
53
- function rawDataToBase64(data) {
54
- if (typeof data === "string")
55
- return Buffer.from(data, "utf8").toString("base64");
56
- if (Buffer.isBuffer(data))
57
- return data.toString("base64");
58
- if (Array.isArray(data))
59
- return Buffer.concat(data).toString("base64");
60
- return Buffer.from(data).toString("base64");
61
- }
62
34
  function normalizeHostToBaseUrl(hostname) {
63
35
  return /^https?:\/\//i.test(hostname) ? hostname : `http://${hostname}`;
64
36
  }
37
+ function parseAllowedCorsOrigins(raw) {
38
+ if (!raw)
39
+ return null;
40
+ const origins = raw
41
+ .split(",")
42
+ .map((origin) => origin.trim().toLowerCase())
43
+ .filter(Boolean);
44
+ return origins.length > 0 ? new Set(origins) : null;
45
+ }
46
+ function isCorsOriginAllowed(origin, allowedOrigins) {
47
+ if (!allowedOrigins)
48
+ return true;
49
+ if (allowedOrigins.has("*"))
50
+ return true;
51
+ return allowedOrigins.has(origin.trim().toLowerCase());
52
+ }
53
+ const ALLOWED_CORS_ORIGINS = parseAllowedCorsOrigins(process.env.TUNNEL_ALLOWED_ORIGINS || process.env.CORS_ALLOWED_ORIGINS);
65
54
  class DainTunnelServer {
66
55
  safeSend(ws, data) {
67
56
  try {
68
- if (ws.readyState === ws_1.default.OPEN) {
57
+ if (ws.readyState === 1) { // WebSocket.OPEN
69
58
  ws.send(JSON.stringify(data));
70
59
  return true;
71
60
  }
@@ -103,55 +92,42 @@ class DainTunnelServer {
103
92
  constructor(hostname, port) {
104
93
  this.hostname = hostname;
105
94
  this.port = port;
95
+ this.server = null;
96
+ this.allowedCorsOrigins = ALLOWED_CORS_ORIGINS;
106
97
  this.tunnels = new Map();
107
98
  this.pendingRequests = new Map();
108
99
  this.challenges = new Map();
109
100
  this.sseConnections = new Map();
110
101
  this.wsConnections = new Map();
111
102
  this.tunnelRequestCount = new Map();
112
- this.app = (0, express_1.default)();
113
- this.server = http_1.default.createServer(this.app);
114
- this.server.keepAliveTimeout = TIMEOUTS.SERVER_KEEPALIVE;
115
- this.server.headersTimeout = TIMEOUTS.SERVER_HEADERS;
116
- this.server.maxHeadersCount = LIMITS.SERVER_MAX_HEADERS;
117
- this.wss = new ws_1.default.Server({
118
- server: this.server,
119
- path: undefined,
120
- backlog: LIMITS.WS_BACKLOG,
121
- perMessageDeflate: false,
122
- maxPayload: LIMITS.MAX_PAYLOAD_BYTES,
123
- });
124
- this.app.use((req, res, next) => {
125
- const origin = req.headers.origin;
103
+ this.app = new Hono();
104
+ this.setupRoutes();
105
+ }
106
+ setupRoutes() {
107
+ // CORS middleware
108
+ this.app.use("*", async (c, next) => {
109
+ const origin = c.req.header("origin");
126
110
  if (origin) {
127
- res.header("Access-Control-Allow-Origin", origin);
128
- res.header("Vary", "Origin");
129
- res.header("Access-Control-Allow-Credentials", "true");
111
+ if (!isCorsOriginAllowed(origin, this.allowedCorsOrigins)) {
112
+ return c.json({ error: "Origin not allowed" }, 403);
113
+ }
114
+ c.header("Access-Control-Allow-Origin", origin);
115
+ c.header("Vary", "Origin");
116
+ c.header("Access-Control-Allow-Credentials", "true");
130
117
  }
131
118
  else {
132
- res.header("Access-Control-Allow-Origin", "*");
119
+ c.header("Access-Control-Allow-Origin", "*");
133
120
  }
134
- res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
135
- 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");
136
- if (req.method === "OPTIONS")
137
- return res.sendStatus(204);
138
- next();
139
- });
140
- this.app.use((req, res, next) => {
141
- if (req.method === 'GET' || req.method === 'HEAD' || req.method === 'OPTIONS') {
142
- return next();
121
+ c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
122
+ c.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, X-Service, X-Service-Token, Content-Type, Authorization, Accept, Origin, X-Requested-With");
123
+ if (c.req.method === "OPTIONS") {
124
+ return c.body(null, 204);
143
125
  }
144
- body_parser_1.default.raw({ type: "*/*", limit: "100mb" })(req, res, next);
126
+ await next();
145
127
  });
146
- this.setupExpressRoutes();
147
- this.setupWebSocketServer();
148
- }
149
- setupExpressRoutes() {
150
- this.app.get("/", (req, res, next) => {
151
- var _a;
152
- if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
153
- return next();
154
- res.status(200).json({
128
+ // Health/status at root
129
+ this.app.get("/", (c) => {
130
+ return c.json({
155
131
  status: "healthy",
156
132
  tunnels: this.tunnels.size,
157
133
  pendingRequests: this.pendingRequests.size,
@@ -160,102 +136,87 @@ class DainTunnelServer {
160
136
  uptime: process.uptime()
161
137
  });
162
138
  });
163
- this.app.get("/health", (req, res) => {
164
- res.status(200).json({
139
+ this.app.get("/health", (c) => {
140
+ return c.json({
165
141
  status: "ok",
166
142
  tunnels: this.tunnels.size,
167
143
  pendingRequests: this.pendingRequests.size,
168
144
  uptime: process.uptime()
169
145
  });
170
146
  });
171
- this.app.use("/:tunnelId", this.handleRequest.bind(this));
147
+ // Tunnel request handler - all methods
148
+ this.app.all("/:tunnelId/*", (c) => this.handleRequest(c));
149
+ this.app.all("/:tunnelId", (c) => this.handleRequest(c));
172
150
  }
173
- setupWebSocketServer() {
174
- this.wss.on("connection", (ws, req) => {
175
- try {
176
- const pathParts = extractPathParts(req.url);
177
- const isRootConnection = pathParts.length <= 1 || !pathParts[1];
178
- if (isRootConnection) {
179
- this.handleTunnelClientConnection(ws, req);
180
- }
181
- else {
182
- this.handleProxiedWebSocketConnection(ws, req);
183
- }
184
- }
185
- catch (error) {
186
- console.error("[Tunnel] WebSocket setup error:", error);
187
- ws.close(1011, "Internal server error");
151
+ // --- WebSocket handlers for Bun.serve ---
152
+ handleWsOpen(ws) {
153
+ // Nothing needed on open for tunnel clients; proxied WS connections
154
+ // are set up via the data attached during upgrade
155
+ }
156
+ handleWsMessage(ws, message) {
157
+ const data = ws.data;
158
+ if (data.isProxiedWebSocket) {
159
+ // This is a proxied WebSocket client connection
160
+ this.handleProxiedWebSocketClientMessage(ws, message);
161
+ return;
162
+ }
163
+ // This is a tunnel client connection
164
+ try {
165
+ const msgStr = typeof message === "string" ? message : Buffer.from(message).toString("utf8");
166
+ const parsed = JSON.parse(msgStr);
167
+ switch (parsed.type) {
168
+ case "challenge_request":
169
+ this.handleChallengeRequest(ws);
170
+ break;
171
+ case "start":
172
+ this.handleStartMessage(ws, parsed);
173
+ break;
174
+ case "response":
175
+ this.handleResponseMessage(parsed);
176
+ break;
177
+ case "sse":
178
+ this.handleSSEMessage(parsed);
179
+ break;
180
+ case "websocket":
181
+ this.handleWebSocketMessage(parsed);
182
+ break;
188
183
  }
189
- });
184
+ }
185
+ catch (error) {
186
+ console.error("[Tunnel] Message error:", error);
187
+ ws.close(1008, "Invalid message");
188
+ }
190
189
  }
191
- handleTunnelClientConnection(ws, req) {
192
- ws.on("message", (message) => {
193
- try {
194
- const data = JSON.parse(rawDataToString(message));
195
- switch (data.type) {
196
- case "challenge_request":
197
- this.handleChallengeRequest(ws);
198
- break;
199
- case "start":
200
- this.handleStartMessage(ws, data);
201
- break;
202
- case "response":
203
- this.handleResponseMessage(data);
204
- break;
205
- case "sse":
206
- this.handleSSEMessage(data);
207
- break;
208
- case "websocket":
209
- this.handleWebSocketMessage(data);
210
- break;
190
+ handleWsClose(ws, code, reason) {
191
+ const data = ws.data;
192
+ if (data.isProxiedWebSocket) {
193
+ // Proxied WS client disconnected
194
+ if (data.proxiedWsId && data.proxiedTunnelId) {
195
+ const tunnel = this.tunnels.get(data.proxiedTunnelId);
196
+ if (tunnel) {
197
+ this.safeSend(tunnel.ws, { type: "websocket", id: data.proxiedWsId, event: "close" });
211
198
  }
199
+ this.wsConnections.delete(data.proxiedWsId);
212
200
  }
213
- catch (error) {
214
- console.error("[Tunnel] Message error:", error);
215
- ws.close(1008, "Invalid message");
216
- }
217
- });
218
- ws.on("close", () => this.removeTunnel(ws));
219
- ws.on("error", (error) => console.error("[Tunnel] WS error:", error));
220
- }
221
- handleProxiedWebSocketConnection(ws, req) {
222
- const pathParts = extractPathParts(req.url);
223
- if (pathParts.length < 2) {
224
- ws.close(1008, "Invalid tunnel ID");
225
201
  return;
226
202
  }
227
- const tunnelId = pathParts[1];
228
- const remainingPath = '/' + pathParts.slice(2).join('/');
229
- const tunnel = this.tunnels.get(tunnelId);
230
- if (!tunnel) {
231
- ws.close(1008, "Tunnel not found");
203
+ // Tunnel client disconnected
204
+ this.removeTunnel(ws);
205
+ }
206
+ // --- Proxied WebSocket handling ---
207
+ handleProxiedWebSocketClientMessage(ws, message) {
208
+ const { proxiedWsId, proxiedTunnelId } = ws.data;
209
+ if (!proxiedWsId || !proxiedTunnelId)
232
210
  return;
233
- }
234
- const wsConnectionId = fastId();
235
- const forwardedHeaders = this.buildForwardedHeaders(req.headers, tunnelId);
236
- this.wsConnections.set(wsConnectionId, {
237
- clientSocket: ws,
238
- id: wsConnectionId,
239
- path: remainingPath,
240
- headers: forwardedHeaders,
241
- tunnelId
242
- });
243
- this.safeSend(tunnel.ws, {
244
- type: "websocket_connection",
245
- id: wsConnectionId,
246
- path: remainingPath,
247
- headers: forwardedHeaders
248
- });
249
- const sendToTunnel = (event, data) => {
250
- const currentTunnel = this.tunnels.get(tunnelId);
251
- if (currentTunnel) {
252
- this.safeSend(currentTunnel.ws, { type: "websocket", id: wsConnectionId, event, data });
253
- }
254
- };
255
- ws.on("message", (data) => sendToTunnel("message", rawDataToBase64(data)));
256
- ws.on("close", () => { sendToTunnel("close"); this.wsConnections.delete(wsConnectionId); });
257
- ws.on("error", (error) => sendToTunnel("error", error.message));
211
+ const tunnel = this.tunnels.get(proxiedTunnelId);
212
+ if (!tunnel)
213
+ return;
214
+ const base64Data = typeof message === "string"
215
+ ? Buffer.from(message, "utf8").toString("base64")
216
+ : Buffer.from(message).toString("base64");
217
+ this.safeSend(tunnel.ws, { type: "websocket", id: proxiedWsId, event: "message", data: base64Data });
258
218
  }
219
+ // --- Tunnel client message handlers ---
259
220
  handleChallengeRequest(ws) {
260
221
  const challenge = fastId();
261
222
  this.challenges.set(challenge, { ws, challenge, timestamp: Date.now() });
@@ -275,16 +236,16 @@ class DainTunnelServer {
275
236
  ws.close(1008, "API key required");
276
237
  return;
277
238
  }
278
- const parsed = (0, auth_1.parseAPIKey)(apiKey);
239
+ const parsed = parseAPIKey(apiKey);
279
240
  if (!parsed) {
280
241
  ws.close(1008, "Invalid API key format");
281
242
  return;
282
243
  }
283
- const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret).update(challenge).digest('hex');
244
+ const expectedSignature = createHmac('sha256', parsed.secret).update(challenge).digest('hex');
284
245
  const expectedSigBuffer = Buffer.from(expectedSignature, 'hex');
285
246
  const receivedSigBuffer = Buffer.from(signature, 'hex');
286
247
  if (expectedSigBuffer.length !== receivedSigBuffer.length ||
287
- !(0, crypto_1.timingSafeEqual)(expectedSigBuffer, receivedSigBuffer)) {
248
+ !timingSafeEqual(expectedSigBuffer, receivedSigBuffer)) {
288
249
  ws.close(1008, "Invalid signature");
289
250
  return;
290
251
  }
@@ -298,19 +259,20 @@ class DainTunnelServer {
298
259
  existingTunnel.ws.close(1000, "Replaced by new connection");
299
260
  }
300
261
  this.tunnels.set(tunnelId, { id: tunnelId, ws });
301
- ws.tunnelId = tunnelId;
262
+ ws.data.tunnelId = tunnelId;
302
263
  let isAlive = true;
303
264
  let missedPongs = 0;
304
- ws.on("pong", () => { isAlive = true; missedPongs = 0; });
265
+ // Bun ServerWebSocket doesn't have pong events or ping() method the same way.
266
+ // Use a simple send-based keepalive check instead.
305
267
  const intervalId = setInterval(() => {
306
268
  if (!this.tunnels.has(tunnelId)) {
307
269
  clearInterval(intervalId);
308
270
  return;
309
271
  }
310
272
  const tunnel = this.tunnels.get(tunnelId);
311
- if (!tunnel || tunnel.ws !== ws || ws.readyState !== ws_1.default.OPEN) {
273
+ if (!tunnel || tunnel.ws !== ws || ws.readyState !== 1) {
312
274
  clearInterval(intervalId);
313
- if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws)
275
+ if (tunnel?.ws === ws)
314
276
  this.tunnels.delete(tunnelId);
315
277
  return;
316
278
  }
@@ -319,24 +281,29 @@ class DainTunnelServer {
319
281
  if (missedPongs >= LIMITS.MAX_MISSED_PONGS) {
320
282
  console.log(`[Tunnel] ${tunnelId} failed liveness check (${missedPongs} missed pongs), terminating`);
321
283
  clearInterval(intervalId);
322
- ws.terminate();
284
+ ws.close(1001, "Liveness check failed");
323
285
  return;
324
286
  }
325
287
  }
326
288
  isAlive = false;
327
289
  try {
290
+ // Bun supports ws.ping() on ServerWebSocket
328
291
  ws.ping();
329
292
  }
330
- catch (_a) {
293
+ catch {
331
294
  clearInterval(intervalId);
332
- ws.terminate();
295
+ ws.close(1001, "Ping failed");
333
296
  }
334
297
  }, TIMEOUTS.PING_INTERVAL);
335
- ws.keepAliveInterval = intervalId;
336
- ws.on("close", () => clearInterval(intervalId));
298
+ ws.data.keepAliveInterval = intervalId;
299
+ // Bun's pong is received via the websocket handler pong callback
300
+ // We track isAlive through the pong handler set up in Bun.serve websocket config
337
301
  const tunnelUrl = this.buildTunnelUrl(tunnelId);
338
302
  ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
339
303
  console.log(`[Tunnel] Created: ${tunnelUrl}`);
304
+ // Store isAlive/missedPongs on ws.data for the pong handler
305
+ ws.data._isAlive = true;
306
+ ws.data._missedPongsReset = () => { isAlive = true; missedPongs = 0; };
340
307
  }
341
308
  catch (error) {
342
309
  console.error(`Error in handleStartMessage for tunnel ${tunnelId}:`, error);
@@ -347,56 +314,59 @@ class DainTunnelServer {
347
314
  const pendingRequest = this.pendingRequests.get(data.requestId);
348
315
  if (!pendingRequest)
349
316
  return;
350
- const { res, tunnelId, timeoutId } = pendingRequest;
317
+ const { resolve, tunnelId, timeoutId } = pendingRequest;
351
318
  this.pendingRequests.delete(data.requestId);
352
319
  if (timeoutId)
353
320
  clearTimeout(timeoutId);
354
321
  this.decrementRequestCount(tunnelId);
355
- // Modify headers in place instead of spreading
356
322
  const headers = data.headers;
357
323
  delete headers["transfer-encoding"];
358
324
  delete headers["content-length"];
359
325
  const bodyBuffer = Buffer.from(data.body, "base64");
360
- res.status(data.status).set(headers).set("Content-Length", bodyBuffer.length.toString()).send(bodyBuffer);
326
+ headers["content-length"] = bodyBuffer.length.toString();
327
+ resolve(new Response(bodyBuffer, {
328
+ status: data.status,
329
+ headers,
330
+ }));
361
331
  }
362
332
  handleSSEMessage(data) {
363
333
  const connection = this.sseConnections.get(data.id);
364
- if (!connection)
334
+ if (!connection || connection.closed)
365
335
  return;
366
- const { res, tunnelId } = connection;
367
336
  if (data.event === 'connected')
368
337
  return;
369
338
  if (data.event === 'close') {
370
339
  try {
371
- res.end();
340
+ connection.controller.close();
372
341
  }
373
- catch (_a) { }
374
- this.cleanupSSEConnection(data.id, tunnelId);
342
+ catch { }
343
+ connection.closed = true;
344
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
375
345
  return;
376
346
  }
377
347
  if (data.event === 'error') {
378
348
  try {
379
- res.write(`event: error\ndata: ${data.data}\n\n`);
380
- res.end();
349
+ connection.controller.enqueue(new TextEncoder().encode(`event: error\ndata: ${data.data}\n\n`));
350
+ connection.controller.close();
381
351
  }
382
- catch (_b) { }
383
- this.cleanupSSEConnection(data.id, tunnelId);
352
+ catch { }
353
+ connection.closed = true;
354
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
384
355
  return;
385
356
  }
386
357
  try {
387
- // Batch write SSE event in single call
388
358
  const lines = data.data.split('\n');
389
359
  const message = (data.event ? `event: ${data.event}\n` : '') +
390
- lines.map(line => `data: ${line}`).join('\n') + '\n\n';
391
- res.write(message);
360
+ lines.map((line) => `data: ${line}`).join('\n') + '\n\n';
361
+ connection.controller.enqueue(new TextEncoder().encode(message));
392
362
  }
393
- catch (_c) {
394
- this.cleanupSSEConnection(data.id, tunnelId);
363
+ catch {
364
+ this.cleanupSSEConnection(data.id, connection.tunnelId);
395
365
  }
396
366
  }
397
367
  cleanupSSEConnection(id, tunnelId) {
398
368
  const connection = this.sseConnections.get(id);
399
- if (connection === null || connection === void 0 ? void 0 : connection.keepAliveInterval)
369
+ if (connection?.keepAliveInterval)
400
370
  clearInterval(connection.keepAliveInterval);
401
371
  this.decrementRequestCount(tunnelId);
402
372
  this.sseConnections.delete(id);
@@ -406,7 +376,7 @@ class DainTunnelServer {
406
376
  if (!connection)
407
377
  return;
408
378
  const { clientSocket } = connection;
409
- const isOpen = clientSocket.readyState === ws_1.default.OPEN;
379
+ const isOpen = clientSocket.readyState === 1;
410
380
  switch (data.event) {
411
381
  case 'message':
412
382
  if (data.data && isOpen)
@@ -424,17 +394,13 @@ class DainTunnelServer {
424
394
  break;
425
395
  }
426
396
  }
427
- async handleRequest(req, res) {
428
- var _a, _b;
429
- const tunnelId = req.params.tunnelId;
430
- if (((_a = req.headers.upgrade) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'websocket')
431
- return;
397
+ async handleRequest(c) {
398
+ const tunnelId = c.req.param("tunnelId");
432
399
  if (!tunnelId || !tunnelId.includes("_")) {
433
- res.status(404).json({
400
+ return c.json({
434
401
  error: "Not Found",
435
402
  message: `Tunnel "${tunnelId}" does not exist.`,
436
- });
437
- return;
403
+ }, 404);
438
404
  }
439
405
  let tunnel;
440
406
  let retries = LIMITS.TUNNEL_RETRY_COUNT;
@@ -446,142 +412,161 @@ class DainTunnelServer {
446
412
  }
447
413
  }
448
414
  if (!tunnel) {
449
- res.status(502).json({
415
+ return c.json({
450
416
  error: "Bad Gateway",
451
417
  message: `Tunnel "${tunnelId}" not connected. The service may be offline or reconnecting.`,
452
418
  availableTunnels: this.tunnels.size
453
- });
454
- return;
455
- }
456
- if (tunnel.ws.bufferedAmount > LIMITS.BACKPRESSURE_THRESHOLD) {
457
- res.status(503).json({ error: "Service Unavailable", message: "Tunnel high load" });
458
- return;
419
+ }, 502);
459
420
  }
460
421
  const currentCount = this.tunnelRequestCount.get(tunnelId) || 0;
461
422
  if (currentCount >= LIMITS.MAX_CONCURRENT_REQUESTS_PER_TUNNEL) {
462
- res.status(503).json({ error: "Service Unavailable", message: "Too many concurrent requests" });
463
- return;
423
+ return c.json({ error: "Service Unavailable", message: "Too many concurrent requests" }, 503);
464
424
  }
465
425
  this.tunnelRequestCount.set(tunnelId, currentCount + 1);
466
- if ((_b = req.headers.accept) === null || _b === void 0 ? void 0 : _b.includes('text/event-stream')) {
467
- this.handleSSERequest(req, res, tunnelId, tunnel);
468
- return;
426
+ // Check for SSE
427
+ if (c.req.header("accept")?.includes('text/event-stream')) {
428
+ return this.handleSSERequest(c, tunnelId, tunnel);
469
429
  }
430
+ // Regular HTTP request
470
431
  const requestId = fastId();
471
- const startTime = Date.now();
472
- const timeoutId = setTimeout(() => {
473
- const pendingRequest = this.pendingRequests.get(requestId);
474
- if (pendingRequest) {
475
- this.decrementRequestCount(tunnelId);
476
- this.pendingRequests.delete(requestId);
477
- if (!res.headersSent) {
478
- res.status(504).json({ error: "Gateway Timeout", message: "Request timed out" });
479
- }
480
- }
481
- }, TIMEOUTS.REQUEST_TIMEOUT);
482
- this.pendingRequests.set(requestId, { res, startTime, tunnelId, timeoutId });
483
- const hasBody = req.method !== "GET" && req.method !== "HEAD" &&
484
- req.body && Buffer.isBuffer(req.body) && req.body.length > 0;
485
- const sent = this.safeSend(tunnel.ws, {
486
- type: "request",
487
- id: requestId,
488
- method: req.method,
489
- path: req.url,
490
- headers: this.buildForwardedHeaders(req.headers, tunnelId),
491
- body: hasBody ? req.body.toString("base64") : undefined,
432
+ // Extract headers as a plain object
433
+ const reqHeaders = {};
434
+ c.req.raw.headers.forEach((value, key) => {
435
+ reqHeaders[key] = value;
492
436
  });
493
- if (!sent) {
494
- clearTimeout(timeoutId);
495
- this.pendingRequests.delete(requestId);
496
- this.decrementRequestCount(tunnelId);
497
- if (!res.headersSent) {
498
- res.status(502).json({ error: "Bad Gateway", message: "Tunnel connection lost" });
437
+ const forwardedHeaders = this.buildForwardedHeaders(reqHeaders, tunnelId);
438
+ // Get the path relative to the tunnel ID (everything after /:tunnelId)
439
+ const fullUrl = new URL(c.req.url);
440
+ const tunnelPrefix = `/${tunnelId}`;
441
+ let path = fullUrl.pathname.slice(tunnelPrefix.length) || '/';
442
+ if (fullUrl.search)
443
+ path += fullUrl.search;
444
+ // Read body for non-GET/HEAD methods
445
+ let bodyBase64;
446
+ const method = c.req.method;
447
+ if (method !== "GET" && method !== "HEAD") {
448
+ try {
449
+ const bodyBuffer = await c.req.arrayBuffer();
450
+ if (bodyBuffer.byteLength > 0) {
451
+ bodyBase64 = Buffer.from(bodyBuffer).toString("base64");
452
+ }
499
453
  }
454
+ catch { }
500
455
  }
456
+ return new Promise((resolve) => {
457
+ const timeoutId = setTimeout(() => {
458
+ const pendingRequest = this.pendingRequests.get(requestId);
459
+ if (pendingRequest) {
460
+ this.decrementRequestCount(tunnelId);
461
+ this.pendingRequests.delete(requestId);
462
+ resolve(new Response(JSON.stringify({ error: "Gateway Timeout", message: "Request timed out" }), {
463
+ status: 504,
464
+ headers: { "content-type": "application/json" },
465
+ }));
466
+ }
467
+ }, TIMEOUTS.REQUEST_TIMEOUT);
468
+ this.pendingRequests.set(requestId, { resolve, startTime: Date.now(), tunnelId, timeoutId });
469
+ const sent = this.safeSend(tunnel.ws, {
470
+ type: "request",
471
+ id: requestId,
472
+ method,
473
+ path,
474
+ headers: forwardedHeaders,
475
+ body: bodyBase64,
476
+ });
477
+ if (!sent) {
478
+ clearTimeout(timeoutId);
479
+ this.pendingRequests.delete(requestId);
480
+ this.decrementRequestCount(tunnelId);
481
+ resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel connection lost" }), {
482
+ status: 502,
483
+ headers: { "content-type": "application/json" },
484
+ }));
485
+ }
486
+ });
501
487
  }
502
- handleSSERequest(req, res, tunnelId, tunnel) {
503
- var _a, _b;
488
+ handleSSERequest(c, tunnelId, tunnel) {
504
489
  const sseId = fastId();
505
- if (req.socket) {
506
- req.socket.setNoDelay(true);
507
- req.socket.setTimeout(0);
508
- }
509
- const origin = req.headers.origin;
510
- const sseHeaders = {
490
+ // Extract headers
491
+ const reqHeaders = {};
492
+ c.req.raw.headers.forEach((value, key) => {
493
+ reqHeaders[key] = value;
494
+ });
495
+ const forwardedHeaders = this.buildForwardedHeaders(reqHeaders, tunnelId);
496
+ const origin = c.req.header("origin");
497
+ const responseHeaders = {
511
498
  'Content-Type': 'text/event-stream',
512
499
  'Cache-Control': 'no-cache, no-transform',
513
500
  'Connection': 'keep-alive',
514
501
  'X-Accel-Buffering': 'no',
515
502
  'X-Content-Type-Options': 'nosniff',
516
503
  'Content-Encoding': 'identity',
517
- 'Transfer-Encoding': 'chunked',
518
504
  };
519
- if (typeof origin === 'string') {
520
- sseHeaders['Access-Control-Allow-Origin'] = origin;
521
- sseHeaders['Access-Control-Allow-Credentials'] = 'true';
522
- sseHeaders['Vary'] = 'Origin';
505
+ if (origin) {
506
+ responseHeaders['Access-Control-Allow-Origin'] = origin;
507
+ responseHeaders['Access-Control-Allow-Credentials'] = 'true';
508
+ responseHeaders['Vary'] = 'Origin';
523
509
  }
524
510
  else {
525
- sseHeaders['Access-Control-Allow-Origin'] = '*';
526
- }
527
- res.writeHead(200, sseHeaders);
528
- const flush = () => {
529
- if (typeof res.flush === 'function')
530
- res.flush();
531
- };
532
- (_a = res.flushHeaders) === null || _a === void 0 ? void 0 : _a.call(res);
533
- res.write(':keepalive\n\n');
534
- flush();
535
- const keepAliveInterval = setInterval(() => {
536
- try {
537
- if (!res.writableEnded) {
538
- res.write(':keepalive\n\n');
539
- flush();
511
+ responseHeaders['Access-Control-Allow-Origin'] = '*';
512
+ }
513
+ const encoder = new TextEncoder();
514
+ let sseController;
515
+ const stream = new ReadableStream({
516
+ start: (controller) => {
517
+ sseController = controller;
518
+ // Send initial keepalive
519
+ controller.enqueue(encoder.encode(':keepalive\n\n'));
520
+ const keepAliveInterval = setInterval(() => {
521
+ const conn = this.sseConnections.get(sseId);
522
+ if (!conn || conn.closed) {
523
+ clearInterval(keepAliveInterval);
524
+ return;
525
+ }
526
+ try {
527
+ controller.enqueue(encoder.encode(':keepalive\n\n'));
528
+ }
529
+ catch {
530
+ clearInterval(keepAliveInterval);
531
+ }
532
+ }, TIMEOUTS.SSE_KEEPALIVE);
533
+ this.sseConnections.set(sseId, {
534
+ controller,
535
+ id: sseId,
536
+ tunnelId,
537
+ keepAliveInterval,
538
+ closed: false,
539
+ });
540
+ const sent = this.safeSend(tunnel.ws, {
541
+ type: "sse_connection",
542
+ id: sseId,
543
+ path: new URL(c.req.url).pathname.slice(`/${tunnelId}`.length) || '/',
544
+ method: c.req.method,
545
+ headers: forwardedHeaders,
546
+ });
547
+ if (!sent) {
548
+ this.cleanupSSEConnection(sseId, tunnelId);
549
+ try {
550
+ controller.close();
551
+ }
552
+ catch { }
540
553
  }
541
- else
542
- clearInterval(keepAliveInterval);
543
- }
544
- catch (_a) {
545
- clearInterval(keepAliveInterval);
546
- }
547
- }, TIMEOUTS.SSE_KEEPALIVE);
548
- this.sseConnections.set(sseId, { req, res, id: sseId, tunnelId, keepAliveInterval });
549
- const hasBody = req.method !== "GET" && req.body &&
550
- Buffer.isBuffer(req.body) && req.body.length > 0;
551
- const sent = this.safeSend(tunnel.ws, {
552
- type: "sse_connection",
553
- id: sseId,
554
- path: req.url,
555
- method: req.method,
556
- headers: this.buildForwardedHeaders(req.headers, tunnelId),
557
- body: hasBody ? req.body.toString("base64") : undefined
554
+ },
555
+ cancel: () => {
556
+ // Client disconnected
557
+ this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
558
+ this.cleanupSSEConnection(sseId, tunnelId);
559
+ },
558
560
  });
559
- if (!sent) {
560
- this.cleanupSSEConnection(sseId, tunnelId);
561
- res.end();
562
- return;
563
- }
564
- let cleanedUp = false;
565
- const doCleanup = () => {
566
- if (cleanedUp)
567
- return;
568
- cleanedUp = true;
569
- this.safeSend(tunnel.ws, { type: "sse_close", id: sseId });
570
- this.cleanupSSEConnection(sseId, tunnelId);
571
- };
572
- req.on('close', () => {
573
- var _a;
574
- if (((_a = req.socket) === null || _a === void 0 ? void 0 : _a.destroyed) || res.writableEnded)
575
- doCleanup();
561
+ return new Response(stream, {
562
+ status: 200,
563
+ headers: responseHeaders,
576
564
  });
577
- (_b = req.socket) === null || _b === void 0 ? void 0 : _b.on('close', doCleanup);
578
- req.on('error', doCleanup);
579
- res.on('error', doCleanup);
580
565
  }
581
566
  removeTunnel(ws) {
582
567
  try {
583
- if (ws.keepAliveInterval)
584
- clearInterval(ws.keepAliveInterval);
568
+ if (ws.data.keepAliveInterval)
569
+ clearInterval(ws.data.keepAliveInterval);
585
570
  const removedTunnelId = this.findAndRemoveTunnel(ws);
586
571
  if (removedTunnelId) {
587
572
  this.tunnelRequestCount.delete(removedTunnelId);
@@ -596,10 +581,10 @@ class DainTunnelServer {
596
581
  }
597
582
  }
598
583
  findAndRemoveTunnel(ws) {
599
- const tunnelId = ws.tunnelId;
584
+ const tunnelId = ws.data.tunnelId;
600
585
  if (tunnelId && this.tunnels.has(tunnelId)) {
601
586
  const tunnel = this.tunnels.get(tunnelId);
602
- if ((tunnel === null || tunnel === void 0 ? void 0 : tunnel.ws) === ws) {
587
+ if (tunnel?.ws === ws) {
603
588
  this.tunnels.delete(tunnelId);
604
589
  return tunnelId;
605
590
  }
@@ -615,14 +600,12 @@ class DainTunnelServer {
615
600
  cleanupPendingRequests(tunnelId) {
616
601
  for (const [requestId, pending] of this.pendingRequests.entries()) {
617
602
  if (pending.tunnelId === tunnelId) {
618
- try {
619
- if (pending.timeoutId)
620
- clearTimeout(pending.timeoutId);
621
- if (!pending.res.headersSent) {
622
- pending.res.status(502).json({ error: "Bad Gateway", message: "Tunnel closed" });
623
- }
624
- }
625
- catch (_a) { }
603
+ if (pending.timeoutId)
604
+ clearTimeout(pending.timeoutId);
605
+ pending.resolve(new Response(JSON.stringify({ error: "Bad Gateway", message: "Tunnel closed" }), {
606
+ status: 502,
607
+ headers: { "content-type": "application/json" },
608
+ }));
626
609
  this.pendingRequests.delete(requestId);
627
610
  }
628
611
  }
@@ -633,9 +616,10 @@ class DainTunnelServer {
633
616
  if (conn.keepAliveInterval)
634
617
  clearInterval(conn.keepAliveInterval);
635
618
  try {
636
- conn.res.end();
619
+ conn.controller.close();
637
620
  }
638
- catch (_a) { }
621
+ catch { }
622
+ conn.closed = true;
639
623
  this.sseConnections.delete(sseId);
640
624
  }
641
625
  }
@@ -646,7 +630,7 @@ class DainTunnelServer {
646
630
  try {
647
631
  conn.clientSocket.close(1001, "Tunnel closed");
648
632
  }
649
- catch (_a) { }
633
+ catch { }
650
634
  this.wsConnections.delete(wsId);
651
635
  }
652
636
  }
@@ -659,78 +643,159 @@ class DainTunnelServer {
659
643
  }
660
644
  }
661
645
  async start() {
662
- return new Promise((resolve) => {
663
- this.server.listen(this.port, () => {
664
- console.log(`DainTunnel Server is running on ${this.hostname}:${this.port}`);
665
- resolve();
666
- });
667
- });
668
- }
669
- async stop() {
670
- return new Promise((resolve) => {
671
- try {
672
- for (const tunnel of this.tunnels.values()) {
673
- if (tunnel.ws.keepAliveInterval)
674
- clearInterval(tunnel.ws.keepAliveInterval);
675
- try {
676
- tunnel.ws.terminate();
646
+ const self = this;
647
+ this.server = Bun.serve({
648
+ port: this.port,
649
+ hostname: "0.0.0.0",
650
+ fetch(req, server) {
651
+ const url = new URL(req.url);
652
+ // Check for WebSocket upgrade
653
+ if (req.headers.get('upgrade')?.toLowerCase() === 'websocket') {
654
+ const pathParts = url.pathname.split('/').filter(Boolean);
655
+ if (pathParts.length === 0) {
656
+ // Root WebSocket = tunnel client connection
657
+ const success = server.upgrade(req, {
658
+ data: {
659
+ isProxiedWebSocket: false,
660
+ tunnelId: undefined,
661
+ keepAliveInterval: undefined,
662
+ },
663
+ });
664
+ if (success)
665
+ return undefined;
666
+ return new Response("WebSocket upgrade failed", { status: 400 });
667
+ }
668
+ // Proxied WebSocket = /:tunnelId/...
669
+ const tunnelId = pathParts[0];
670
+ const remainingPath = '/' + pathParts.slice(1).join('/');
671
+ const tunnel = self.tunnels.get(tunnelId);
672
+ if (!tunnel) {
673
+ return new Response("Tunnel not found", { status: 404 });
677
674
  }
678
- catch (error) {
679
- console.error(`Error closing tunnel ${tunnel.id}:`, error);
675
+ const wsConnectionId = fastId();
676
+ const reqHeaders = {};
677
+ req.headers.forEach((value, key) => {
678
+ reqHeaders[key] = value;
679
+ });
680
+ const forwardedHeaders = self.buildForwardedHeaders(reqHeaders, tunnelId);
681
+ const success = server.upgrade(req, {
682
+ data: {
683
+ isProxiedWebSocket: true,
684
+ proxiedWsId: wsConnectionId,
685
+ proxiedTunnelId: tunnelId,
686
+ },
687
+ });
688
+ if (success) {
689
+ // Store setup info so open handler can register the connection
690
+ self._pendingProxiedWs = self._pendingProxiedWs || new Map();
691
+ self._pendingProxiedWs.set(wsConnectionId, {
692
+ tunnelId,
693
+ path: remainingPath,
694
+ headers: forwardedHeaders,
695
+ });
696
+ self.safeSend(tunnel.ws, {
697
+ type: "websocket_connection",
698
+ id: wsConnectionId,
699
+ path: remainingPath,
700
+ headers: forwardedHeaders,
701
+ });
702
+ return undefined;
680
703
  }
704
+ return new Response("WebSocket upgrade failed", { status: 400 });
681
705
  }
682
- this.tunnels.clear();
683
- this.tunnelRequestCount.clear();
684
- for (const [requestId, pending] of this.pendingRequests.entries()) {
685
- if (pending.timeoutId)
686
- clearTimeout(pending.timeoutId);
687
- if (!pending.res.headersSent) {
688
- try {
689
- pending.res.status(503).json({ error: "Service Unavailable", message: "Server shutting down" });
706
+ // Regular HTTP request - pass to Hono
707
+ return self.app.fetch(req);
708
+ },
709
+ websocket: {
710
+ open(ws) {
711
+ if (ws.data.isProxiedWebSocket && ws.data.proxiedWsId) {
712
+ const pendingMap = self._pendingProxiedWs;
713
+ const pending = pendingMap?.get(ws.data.proxiedWsId);
714
+ if (pending) {
715
+ self.wsConnections.set(ws.data.proxiedWsId, {
716
+ clientSocket: ws,
717
+ id: ws.data.proxiedWsId,
718
+ path: pending.path,
719
+ headers: pending.headers,
720
+ tunnelId: pending.tunnelId,
721
+ });
722
+ pendingMap.delete(ws.data.proxiedWsId);
690
723
  }
691
- catch (_a) { }
692
724
  }
693
- this.pendingRequests.delete(requestId);
725
+ self.handleWsOpen(ws);
726
+ },
727
+ message(ws, message) {
728
+ self.handleWsMessage(ws, message);
729
+ },
730
+ close(ws, code, reason) {
731
+ self.handleWsClose(ws, code, reason);
732
+ },
733
+ pong(ws) {
734
+ // Reset liveness tracking when pong received
735
+ const resetFn = ws.data?._missedPongsReset;
736
+ if (resetFn)
737
+ resetFn();
738
+ },
739
+ perMessageDeflate: false,
740
+ maxPayloadLength: LIMITS.MAX_PAYLOAD_BYTES,
741
+ backpressureLimit: LIMITS.BACKPRESSURE_THRESHOLD,
742
+ },
743
+ });
744
+ console.log(`DainTunnel Server is running on ${this.hostname}:${this.port}`);
745
+ }
746
+ async stop() {
747
+ try {
748
+ for (const tunnel of this.tunnels.values()) {
749
+ if (tunnel.ws.data.keepAliveInterval)
750
+ clearInterval(tunnel.ws.data.keepAliveInterval);
751
+ try {
752
+ tunnel.ws.close(1001, "Server shutting down");
694
753
  }
695
- this.challenges.clear();
696
- for (const [sseId, conn] of this.sseConnections.entries()) {
697
- if (conn.keepAliveInterval)
698
- clearInterval(conn.keepAliveInterval);
699
- try {
700
- conn.res.end();
701
- }
702
- catch (error) {
703
- console.error(`Error closing SSE ${sseId}:`, error);
704
- }
705
- this.sseConnections.delete(sseId);
754
+ catch (error) {
755
+ console.error(`Error closing tunnel ${tunnel.id}:`, error);
706
756
  }
707
- for (const [wsId, conn] of this.wsConnections.entries()) {
708
- try {
709
- conn.clientSocket.close(1001, "Server shutting down");
710
- }
711
- catch (error) {
712
- console.error(`Error closing WS ${wsId}:`, error);
713
- }
714
- this.wsConnections.delete(wsId);
757
+ }
758
+ this.tunnels.clear();
759
+ this.tunnelRequestCount.clear();
760
+ for (const [requestId, pending] of this.pendingRequests.entries()) {
761
+ if (pending.timeoutId)
762
+ clearTimeout(pending.timeoutId);
763
+ pending.resolve(new Response(JSON.stringify({ error: "Service Unavailable", message: "Server shutting down" }), {
764
+ status: 503,
765
+ headers: { "content-type": "application/json" },
766
+ }));
767
+ this.pendingRequests.delete(requestId);
768
+ }
769
+ this.challenges.clear();
770
+ for (const [sseId, conn] of this.sseConnections.entries()) {
771
+ if (conn.keepAliveInterval)
772
+ clearInterval(conn.keepAliveInterval);
773
+ try {
774
+ conn.controller.close();
715
775
  }
716
- for (const socket of this.wss.clients) {
717
- try {
718
- socket.terminate();
719
- }
720
- catch (_b) { }
776
+ catch (error) {
777
+ console.error(`Error closing SSE ${sseId}:`, error);
721
778
  }
722
- this.wss.close(() => {
723
- var _a, _b, _c, _d;
724
- (_b = (_a = this.server).closeIdleConnections) === null || _b === void 0 ? void 0 : _b.call(_a);
725
- (_d = (_c = this.server).closeAllConnections) === null || _d === void 0 ? void 0 : _d.call(_c);
726
- this.server.close(() => resolve());
727
- });
779
+ conn.closed = true;
780
+ this.sseConnections.delete(sseId);
728
781
  }
729
- catch (error) {
730
- console.error('Error during server shutdown:', error);
731
- resolve();
782
+ for (const [wsId, conn] of this.wsConnections.entries()) {
783
+ try {
784
+ conn.clientSocket.close(1001, "Server shutting down");
785
+ }
786
+ catch (error) {
787
+ console.error(`Error closing WS ${wsId}:`, error);
788
+ }
789
+ this.wsConnections.delete(wsId);
732
790
  }
733
- });
791
+ if (this.server) {
792
+ this.server.stop();
793
+ this.server = null;
794
+ }
795
+ }
796
+ catch (error) {
797
+ console.error('Error during server shutdown:', error);
798
+ }
734
799
  }
735
800
  }
736
- exports.default = DainTunnelServer;
801
+ export default DainTunnelServer;