@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.
- package/dist/client/index.d.ts +1 -6
- package/dist/client/index.js +120 -184
- package/dist/server/index.d.ts +6 -5
- package/dist/server/index.js +437 -372
- package/dist/server/start.js +4 -13
- package/package.json +8 -24
package/dist/server/index.js
CHANGED
|
@@ -1,20 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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,
|
|
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,
|
|
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 ===
|
|
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 =
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
119
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
133
120
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (req.method === "OPTIONS")
|
|
137
|
-
return
|
|
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
|
-
|
|
126
|
+
await next();
|
|
145
127
|
});
|
|
146
|
-
|
|
147
|
-
this.
|
|
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", (
|
|
164
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
!
|
|
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
|
-
|
|
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 !==
|
|
273
|
+
if (!tunnel || tunnel.ws !== ws || ws.readyState !== 1) {
|
|
312
274
|
clearInterval(intervalId);
|
|
313
|
-
if (
|
|
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.
|
|
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
|
|
293
|
+
catch {
|
|
331
294
|
clearInterval(intervalId);
|
|
332
|
-
ws.
|
|
295
|
+
ws.close(1001, "Ping failed");
|
|
333
296
|
}
|
|
334
297
|
}, TIMEOUTS.PING_INTERVAL);
|
|
335
|
-
ws.keepAliveInterval = intervalId;
|
|
336
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
340
|
+
connection.controller.close();
|
|
372
341
|
}
|
|
373
|
-
catch
|
|
374
|
-
|
|
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
|
-
|
|
380
|
-
|
|
349
|
+
connection.controller.enqueue(new TextEncoder().encode(`event: error\ndata: ${data.data}\n\n`));
|
|
350
|
+
connection.controller.close();
|
|
381
351
|
}
|
|
382
|
-
catch
|
|
383
|
-
|
|
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
|
-
|
|
360
|
+
lines.map((line) => `data: ${line}`).join('\n') + '\n\n';
|
|
361
|
+
connection.controller.enqueue(new TextEncoder().encode(message));
|
|
392
362
|
}
|
|
393
|
-
catch
|
|
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
|
|
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 ===
|
|
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(
|
|
428
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
467
|
-
|
|
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
|
-
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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(
|
|
503
|
-
var _a, _b;
|
|
488
|
+
handleSSERequest(c, tunnelId, tunnel) {
|
|
504
489
|
const sseId = fastId();
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
const
|
|
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 (
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
-
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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 (
|
|
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
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
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.
|
|
619
|
+
conn.controller.close();
|
|
637
620
|
}
|
|
638
|
-
catch
|
|
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
|
|
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
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
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
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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
|
-
|
|
717
|
-
|
|
718
|
-
socket.terminate();
|
|
719
|
-
}
|
|
720
|
-
catch (_b) { }
|
|
776
|
+
catch (error) {
|
|
777
|
+
console.error(`Error closing SSE ${sseId}:`, error);
|
|
721
778
|
}
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
801
|
+
export default DainTunnelServer;
|