@dainprotocol/tunnel 1.0.4 → 1.0.7

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.
@@ -7,14 +7,19 @@ declare class DainTunnel extends EventEmitter {
7
7
  private reconnectAttempts;
8
8
  private maxReconnectAttempts;
9
9
  private reconnectDelay;
10
- private auth;
10
+ private apiKey;
11
11
  private tunnelId;
12
+ private webSocketClients;
13
+ private sseClients;
12
14
  constructor(serverUrl: string, apiKey: string);
13
15
  start(port: number): Promise<string>;
14
16
  private connect;
15
- private requestChallenge;
16
17
  private handleMessage;
17
18
  private handleRequest;
19
+ private handleWebSocketConnection;
20
+ private handleWebSocketMessage;
21
+ private handleSSEConnection;
22
+ private handleSSEClose;
18
23
  private forwardRequest;
19
24
  private sendMessage;
20
25
  private attemptReconnect;
@@ -7,8 +7,7 @@ exports.DainTunnel = void 0;
7
7
  const ws_1 = __importDefault(require("ws"));
8
8
  const http_1 = __importDefault(require("http"));
9
9
  const events_1 = require("events");
10
- const bs58_1 = __importDefault(require("bs58"));
11
- const client_1 = require("@dainprotocol/service-sdk/client");
10
+ const crypto_1 = require("crypto");
12
11
  class DainTunnel extends events_1.EventEmitter {
13
12
  constructor(serverUrl, apiKey) {
14
13
  super();
@@ -19,8 +18,12 @@ class DainTunnel extends events_1.EventEmitter {
19
18
  this.reconnectAttempts = 0;
20
19
  this.maxReconnectAttempts = 5;
21
20
  this.reconnectDelay = 5000;
22
- this.auth = new client_1.DainClientAuth({ apiKey });
23
- this.tunnelId = bs58_1.default.encode(this.auth.getPublicKey());
21
+ this.webSocketClients = new Map();
22
+ this.sseClients = new Map();
23
+ this.apiKey = apiKey;
24
+ // Create tunnel ID from API key hash
25
+ const hash = (0, crypto_1.createHash)('sha256').update(apiKey).digest();
26
+ this.tunnelId = hash.toString('base64url').substring(0, 16);
24
27
  }
25
28
  async start(port) {
26
29
  this.port = port;
@@ -28,65 +31,59 @@ class DainTunnel extends events_1.EventEmitter {
28
31
  }
29
32
  async connect() {
30
33
  return new Promise((resolve, reject) => {
31
- this.ws = new ws_1.default(this.serverUrl);
32
- this.ws.on("open", async () => {
33
- this.reconnectAttempts = 0;
34
- const challenge = await this.requestChallenge();
35
- const signature = this.auth.signMessage(challenge);
36
- this.sendMessage({
37
- type: "start",
38
- port: this.port,
39
- challenge,
40
- signature,
41
- tunnelId: this.tunnelId
34
+ try {
35
+ console.log(`Connecting to WebSocket server: ${this.serverUrl}`);
36
+ this.ws = new ws_1.default(this.serverUrl);
37
+ this.ws.on("open", () => {
38
+ console.log('WebSocket connection opened');
39
+ this.reconnectAttempts = 0;
40
+ // Send API key authentication directly (no challenge-response)
41
+ this.sendMessage({
42
+ type: "start",
43
+ port: this.port,
44
+ apiKey: this.apiKey,
45
+ tunnelId: this.tunnelId
46
+ });
47
+ this.emit("connected");
42
48
  });
43
- this.emit("connected");
44
- });
45
- this.ws.on("message", (data) => {
46
- const message = JSON.parse(data);
47
- this.handleMessage(message, resolve);
48
- });
49
- this.ws.on("close", () => {
50
- this.emit("disconnected");
51
- this.attemptReconnect();
52
- });
53
- this.ws.on("error", (error) => {
54
- this.emit("error", error);
55
- reject(error);
56
- });
57
- // Add a timeout to reject the promise if connection takes too long
58
- setTimeout(() => {
59
- var _a;
60
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) !== ws_1.default.OPEN) {
61
- reject(new Error("Connection timeout"));
62
- }
63
- }, 5000);
64
- });
65
- }
66
- async requestChallenge() {
67
- return new Promise((resolve, reject) => {
68
- if (!this.ws) {
69
- reject(new Error("WebSocket is not connected"));
70
- return;
49
+ this.ws.on("message", (data) => {
50
+ try {
51
+ const message = JSON.parse(data);
52
+ this.handleMessage(message, resolve);
53
+ }
54
+ catch (err) {
55
+ console.error('Error handling message:', err);
56
+ reject(err);
57
+ }
58
+ });
59
+ this.ws.on("close", () => {
60
+ console.log('WebSocket connection closed');
61
+ if (this.tunnelUrl) {
62
+ this.emit("disconnected");
63
+ this.attemptReconnect();
64
+ }
65
+ else {
66
+ // If we haven't received a tunnelUrl yet, just reject
67
+ reject(new Error("Connection closed before tunnel was established"));
68
+ }
69
+ });
70
+ this.ws.on("error", (error) => {
71
+ // Prevent unhandled error events
72
+ // Don't reject here, the close handler will be called after an error
73
+ this.emit("error", error);
74
+ });
75
+ // Add a timeout to reject the promise if connection takes too long
76
+ setTimeout(() => {
77
+ if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
78
+ const timeoutError = new Error("Connection timeout");
79
+ reject(timeoutError);
80
+ }
81
+ }, 10000);
82
+ }
83
+ catch (err) {
84
+ console.error('Error creating WebSocket:', err);
85
+ reject(err);
71
86
  }
72
- this.ws.send(JSON.stringify({ type: "challenge_request" }));
73
- const challengeHandler = (message) => {
74
- const data = JSON.parse(message);
75
- if (data.type === "challenge") {
76
- if (this.ws) {
77
- this.ws.removeListener("message", challengeHandler);
78
- }
79
- resolve(data.challenge);
80
- }
81
- };
82
- this.ws.on("message", challengeHandler);
83
- // Add a timeout for the challenge request
84
- setTimeout(() => {
85
- if (this.ws) {
86
- this.ws.removeListener("message", challengeHandler);
87
- }
88
- reject(new Error("Challenge request timeout"));
89
- }, 5000);
90
87
  });
91
88
  }
92
89
  handleMessage(message, resolve) {
@@ -99,6 +96,18 @@ class DainTunnel extends events_1.EventEmitter {
99
96
  case "request":
100
97
  this.handleRequest(message);
101
98
  break;
99
+ case "websocket_connection":
100
+ this.handleWebSocketConnection(message);
101
+ break;
102
+ case "sse_connection":
103
+ this.handleSSEConnection(message);
104
+ break;
105
+ case "sse_close":
106
+ this.handleSSEClose(message);
107
+ break;
108
+ case "websocket":
109
+ this.handleWebSocketMessage(message);
110
+ break;
102
111
  }
103
112
  }
104
113
  async handleRequest(request) {
@@ -111,6 +120,191 @@ class DainTunnel extends events_1.EventEmitter {
111
120
  this.emit("request_error", { request, error });
112
121
  }
113
122
  }
123
+ handleWebSocketConnection(message) {
124
+ try {
125
+ console.log(`Creating local WebSocket connection to: ws://localhost:${this.port}${message.path}`);
126
+ // Create a WebSocket connection to the local server
127
+ const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
128
+ headers: message.headers
129
+ });
130
+ // Store the client
131
+ this.webSocketClients.set(message.id, client);
132
+ // Handle connection open
133
+ client.on('open', () => {
134
+ console.log(`Local WebSocket connection opened: ${message.id}`);
135
+ });
136
+ // Handle messages from the local server
137
+ client.on('message', (data) => {
138
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
139
+ this.ws.send(JSON.stringify({
140
+ type: 'websocket',
141
+ id: message.id,
142
+ event: 'message',
143
+ data: data.toString('base64')
144
+ }));
145
+ }
146
+ });
147
+ // Handle close
148
+ client.on('close', () => {
149
+ console.log(`Local WebSocket connection closed: ${message.id}`);
150
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
151
+ this.ws.send(JSON.stringify({
152
+ type: 'websocket',
153
+ id: message.id,
154
+ event: 'close'
155
+ }));
156
+ }
157
+ this.webSocketClients.delete(message.id);
158
+ });
159
+ // Handle errors
160
+ client.on('error', (error) => {
161
+ console.error(`Local WebSocket connection error: ${message.id}`, error.message);
162
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
163
+ this.ws.send(JSON.stringify({
164
+ type: 'websocket',
165
+ id: message.id,
166
+ event: 'error',
167
+ data: error.message
168
+ }));
169
+ }
170
+ this.webSocketClients.delete(message.id);
171
+ });
172
+ this.emit("websocket_connection", { id: message.id, path: message.path });
173
+ }
174
+ catch (error) {
175
+ console.error('Error establishing WebSocket connection:', error);
176
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
177
+ this.ws.send(JSON.stringify({
178
+ type: 'websocket',
179
+ id: message.id,
180
+ event: 'error',
181
+ data: error.message
182
+ }));
183
+ }
184
+ }
185
+ }
186
+ handleWebSocketMessage(message) {
187
+ const client = this.webSocketClients.get(message.id);
188
+ if (!client)
189
+ return;
190
+ if (message.event === 'message' && message.data) {
191
+ const data = Buffer.from(message.data, 'base64');
192
+ if (client.readyState === ws_1.default.OPEN) {
193
+ client.send(data);
194
+ }
195
+ }
196
+ else if (message.event === 'close') {
197
+ if (client.readyState === ws_1.default.OPEN) {
198
+ client.close();
199
+ }
200
+ this.webSocketClients.delete(message.id);
201
+ }
202
+ }
203
+ handleSSEConnection(message) {
204
+ try {
205
+ // Create an EventSource-like stream to the local server
206
+ // Since Node.js doesn't have a built-in EventSource, we'll use HTTP
207
+ const options = {
208
+ hostname: 'localhost',
209
+ port: this.port,
210
+ path: message.path,
211
+ method: 'GET',
212
+ headers: message.headers,
213
+ };
214
+ const req = http_1.default.request(options, (res) => {
215
+ if (res.statusCode !== 200) {
216
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
217
+ this.ws.send(JSON.stringify({
218
+ type: 'sse',
219
+ id: message.id,
220
+ event: 'error',
221
+ data: `Server responded with status code ${res.statusCode}`
222
+ }));
223
+ }
224
+ return;
225
+ }
226
+ // Process SSE stream
227
+ let buffer = '';
228
+ res.on('data', (chunk) => {
229
+ buffer += chunk.toString();
230
+ // Process complete SSE messages
231
+ while (buffer.includes('\n\n')) {
232
+ const messageEndIndex = buffer.indexOf('\n\n');
233
+ const messageData = buffer.substring(0, messageEndIndex);
234
+ buffer = buffer.substring(messageEndIndex + 2);
235
+ // Parse the SSE message
236
+ const lines = messageData.split('\n');
237
+ let event = 'message';
238
+ let data = '';
239
+ for (const line of lines) {
240
+ if (line.startsWith('event:')) {
241
+ event = line.substring(6).trim();
242
+ }
243
+ else if (line.startsWith('data:')) {
244
+ data += line.substring(5).trim() + '\n';
245
+ }
246
+ }
247
+ // Remove trailing newline
248
+ if (data.endsWith('\n')) {
249
+ data = data.substring(0, data.length - 1);
250
+ }
251
+ // Forward to server
252
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
253
+ this.ws.send(JSON.stringify({
254
+ type: 'sse',
255
+ id: message.id,
256
+ event,
257
+ data
258
+ }));
259
+ }
260
+ }
261
+ });
262
+ res.on('end', () => {
263
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
264
+ this.ws.send(JSON.stringify({
265
+ type: 'sse',
266
+ id: message.id,
267
+ event: 'close',
268
+ data: 'Connection closed'
269
+ }));
270
+ }
271
+ });
272
+ });
273
+ req.on('error', (error) => {
274
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
275
+ this.ws.send(JSON.stringify({
276
+ type: 'sse',
277
+ id: message.id,
278
+ event: 'error',
279
+ data: error.message
280
+ }));
281
+ }
282
+ });
283
+ req.end();
284
+ // Store a reference to abort the connection later if needed
285
+ this.sseClients.set(message.id, req);
286
+ this.emit("sse_connection", { id: message.id, path: message.path });
287
+ }
288
+ catch (error) {
289
+ console.error('Error establishing SSE connection:', error);
290
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
291
+ this.ws.send(JSON.stringify({
292
+ type: 'sse',
293
+ id: message.id,
294
+ event: 'error',
295
+ data: error.message
296
+ }));
297
+ }
298
+ }
299
+ }
300
+ handleSSEClose(message) {
301
+ const client = this.sseClients.get(message.id);
302
+ if (client) {
303
+ // Abort the request if it's still active
304
+ client.destroy();
305
+ this.sseClients.delete(message.id);
306
+ }
307
+ }
114
308
  forwardRequest(request) {
115
309
  return new Promise((resolve, reject) => {
116
310
  const options = {
@@ -153,19 +347,73 @@ class DainTunnel extends events_1.EventEmitter {
153
347
  attemptReconnect() {
154
348
  if (this.reconnectAttempts < this.maxReconnectAttempts) {
155
349
  this.reconnectAttempts++;
156
- setTimeout(() => this.connect(), this.reconnectDelay);
350
+ console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
351
+ setTimeout(async () => {
352
+ try {
353
+ await this.connect();
354
+ // If the connect succeeds but we don't have a tunnelUrl yet, we're still initializing
355
+ if (!this.tunnelUrl) {
356
+ console.log('Reconnected but tunnelUrl not set. Waiting for tunnel URL...');
357
+ }
358
+ else {
359
+ console.log(`Reconnected successfully. Tunnel URL: ${this.tunnelUrl}`);
360
+ }
361
+ }
362
+ catch (error) {
363
+ console.error(`Reconnection attempt ${this.reconnectAttempts} failed:`, error);
364
+ }
365
+ }, this.reconnectDelay);
157
366
  }
158
367
  else {
368
+ console.log('Maximum reconnection attempts reached');
159
369
  this.emit("max_reconnect_attempts");
160
370
  }
161
371
  }
162
372
  async stop() {
163
- return new Promise((resolve) => {
164
- if (this.ws) {
165
- this.ws.close();
166
- this.ws = null;
373
+ return new Promise(async (resolve) => {
374
+ try {
375
+ // Close all WebSocket clients
376
+ for (const [id, client] of this.webSocketClients.entries()) {
377
+ try {
378
+ if (client.readyState === ws_1.default.OPEN) {
379
+ client.close();
380
+ }
381
+ }
382
+ catch (error) {
383
+ console.error(`Error closing WebSocket client ${id}:`, error);
384
+ }
385
+ }
386
+ this.webSocketClients.clear();
387
+ // Close all SSE clients
388
+ for (const [id, client] of this.sseClients.entries()) {
389
+ try {
390
+ client.destroy();
391
+ }
392
+ catch (error) {
393
+ console.error(`Error destroying SSE client ${id}:`, error);
394
+ }
395
+ }
396
+ this.sseClients.clear();
397
+ // Close main WebSocket connection
398
+ if (this.ws) {
399
+ try {
400
+ if (this.ws.readyState === ws_1.default.OPEN) {
401
+ this.ws.close();
402
+ }
403
+ }
404
+ catch (error) {
405
+ console.error('Error closing main WebSocket:', error);
406
+ }
407
+ this.ws = null;
408
+ }
409
+ // Wait for all connections to close properly
410
+ await new Promise(resolve => setTimeout(resolve, 500));
411
+ resolve();
412
+ }
413
+ catch (error) {
414
+ console.error('Error in stop method:', error);
415
+ resolve(); // Resolve anyway to not block cleaning up
167
416
  }
168
- resolve();
169
417
  });
170
418
  }
171
419
  }
@@ -6,14 +6,19 @@ declare class DainTunnelServer {
6
6
  private wss;
7
7
  private tunnels;
8
8
  private pendingRequests;
9
- private challenges;
9
+ private sseConnections;
10
+ private wsConnections;
10
11
  constructor(hostname: string, port: number);
11
12
  private setupExpressRoutes;
12
13
  private setupWebSocketServer;
13
- private handleChallengeRequest;
14
+ private handleTunnelClientConnection;
15
+ private handleProxiedWebSocketConnection;
14
16
  private handleStartMessage;
15
17
  private handleResponseMessage;
16
- private handleHttpRequest;
18
+ private handleSSEMessage;
19
+ private handleWebSocketMessage;
20
+ private handleRequest;
21
+ private handleSSERequest;
17
22
  private removeTunnel;
18
23
  start(): Promise<void>;
19
24
  stop(): Promise<void>;
@@ -9,20 +9,27 @@ const ws_1 = __importDefault(require("ws"));
9
9
  const uuid_1 = require("uuid");
10
10
  const body_parser_1 = __importDefault(require("body-parser"));
11
11
  const cors_1 = __importDefault(require("cors"));
12
- const client_1 = require("@dainprotocol/service-sdk/client");
13
- const bs58_1 = __importDefault(require("bs58"));
12
+ const url_1 = require("url");
14
13
  class DainTunnelServer {
15
14
  constructor(hostname, port) {
16
15
  this.hostname = hostname;
17
16
  this.port = port;
18
17
  this.tunnels = new Map();
19
18
  this.pendingRequests = new Map();
20
- this.challenges = new Map();
19
+ this.sseConnections = new Map();
20
+ this.wsConnections = new Map();
21
21
  this.app = (0, express_1.default)();
22
22
  this.server = http_1.default.createServer(this.app);
23
- this.wss = new ws_1.default.Server({ server: this.server });
23
+ this.wss = new ws_1.default.Server({
24
+ server: this.server,
25
+ path: undefined // Allow connections on any path
26
+ });
24
27
  // Use cors middleware
25
- this.app.use((0, cors_1.default)());
28
+ this.app.use((0, cors_1.default)({
29
+ origin: '*',
30
+ methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
31
+ credentials: true
32
+ }));
26
33
  // Update CORS middleware
27
34
  this.app.use((req, res, next) => {
28
35
  res.header("Access-Control-Allow-Origin", "*");
@@ -39,81 +46,200 @@ class DainTunnelServer {
39
46
  this.setupWebSocketServer();
40
47
  }
41
48
  setupExpressRoutes() {
42
- this.app.use("/:tunnelId", this.handleHttpRequest.bind(this));
49
+ // Generic route handler for all tunnel requests
50
+ this.app.use("/:tunnelId", this.handleRequest.bind(this));
43
51
  }
44
52
  setupWebSocketServer() {
45
- this.wss.on("connection", (ws) => {
53
+ // Handle WebSocket connections from tunnel clients
54
+ this.wss.on("connection", (ws, req) => {
55
+ var _a;
46
56
  console.log("New WebSocket connection");
47
- ws.on("message", (message) => {
48
- try {
49
- const data = JSON.parse(message);
50
- console.log(`Received WebSocket message: ${data.type}`);
51
- if (data.type === "challenge_request") {
52
- this.handleChallengeRequest(ws);
53
- }
54
- else if (data.type === "start") {
55
- this.handleStartMessage(ws, data);
56
- }
57
- else if (data.type === "response") {
58
- this.handleResponseMessage(data);
59
- }
57
+ try {
58
+ // Check if this is a tunnel client connection or a user connection to be proxied
59
+ const url = (0, url_1.parse)(req.url || '', true);
60
+ const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
61
+ console.log(`WebSocket connection path: ${url.pathname}`);
62
+ // Client tunnel connection has no tunnelId in the path (or is at root)
63
+ if (!url.pathname || url.pathname === '/' || pathParts.length <= 1 || !pathParts[1]) {
64
+ console.log("Handling as tunnel client connection");
65
+ this.handleTunnelClientConnection(ws, req);
60
66
  }
61
- catch (error) {
62
- console.error("Error processing message:", error);
63
- ws.close(1008, "Invalid message");
67
+ else {
68
+ // This is a WebSocket connection to be proxied through the tunnel
69
+ console.log(`Handling as proxied WebSocket connection for path: ${url.pathname}`);
70
+ this.handleProxiedWebSocketConnection(ws, req);
64
71
  }
65
- });
66
- ws.on("close", () => {
67
- console.log("WebSocket connection closed");
68
- this.removeTunnel(ws);
69
- });
70
- ws.on("error", (error) => {
71
- console.error("WebSocket error:", error);
72
- });
72
+ }
73
+ catch (error) {
74
+ console.error("Error in WebSocket connection setup:", error);
75
+ ws.close(1011, "Internal server error");
76
+ }
73
77
  });
74
78
  }
75
- handleChallengeRequest(ws) {
76
- const challenge = (0, uuid_1.v4)();
77
- const challengeObj = { ws, challenge };
78
- this.challenges.set(challenge, challengeObj);
79
- ws.send(JSON.stringify({ type: "challenge", challenge }));
79
+ // Handle WebSocket connections from tunnel clients
80
+ handleTunnelClientConnection(ws, req) {
81
+ ws.on("message", (message) => {
82
+ try {
83
+ const data = JSON.parse(message);
84
+ console.log(`Received WebSocket message: ${data.type}`);
85
+ if (data.type === "start") {
86
+ this.handleStartMessage(ws, data);
87
+ }
88
+ else if (data.type === "response") {
89
+ this.handleResponseMessage(data);
90
+ }
91
+ else if (data.type === "sse") {
92
+ this.handleSSEMessage(data);
93
+ }
94
+ else if (data.type === "websocket") {
95
+ this.handleWebSocketMessage(data);
96
+ }
97
+ }
98
+ catch (error) {
99
+ console.error("Error processing message:", error);
100
+ ws.close(1008, "Invalid message");
101
+ }
102
+ });
103
+ ws.on("close", () => {
104
+ console.log("WebSocket connection closed");
105
+ this.removeTunnel(ws);
106
+ });
107
+ ws.on("error", (error) => {
108
+ console.error("WebSocket error:", error);
109
+ });
80
110
  }
81
- handleStartMessage(ws, data) {
82
- const { challenge, signature, tunnelId } = data;
83
- const challengeObj = this.challenges.get(challenge);
84
- if (!challengeObj || challengeObj.ws !== ws) {
85
- ws.close(1008, "Invalid challenge");
111
+ // Handle incoming WebSocket connections to be proxied through the tunnel
112
+ handleProxiedWebSocketConnection(ws, req) {
113
+ var _a;
114
+ const url = (0, url_1.parse)(req.url || '', true);
115
+ const pathParts = ((_a = url.pathname) === null || _a === void 0 ? void 0 : _a.split('/')) || [];
116
+ if (pathParts.length < 2) {
117
+ ws.close(1008, "Invalid tunnel ID");
86
118
  return;
87
119
  }
88
- this.challenges.delete(challenge);
89
- const publicKey = bs58_1.default.decode(tunnelId);
90
- if (!client_1.DainClientAuth.verifyMessage(challenge, signature, publicKey)) {
91
- ws.close(1008, "Invalid signature");
120
+ const tunnelId = pathParts[1];
121
+ const remainingPath = '/' + pathParts.slice(2).join('/');
122
+ const tunnel = this.tunnels.get(tunnelId);
123
+ console.log(`Handling WebSocket connection for tunnel: ${tunnelId}, path: ${remainingPath}`);
124
+ console.log(`Available tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
125
+ if (!tunnel) {
126
+ console.log(`Tunnel not found for WebSocket connection: ${tunnelId}`);
127
+ ws.close(1008, "Tunnel not found");
92
128
  return;
93
129
  }
94
- this.tunnels.set(tunnelId, { id: tunnelId, ws });
95
- console.log(`Tunnel added: ${tunnelId}`);
96
- console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
97
- // Add a periodic check to ensure the tunnel is still in the map
98
- const intervalId = setInterval(() => {
130
+ // Create a WebSocket connection ID
131
+ const wsConnectionId = (0, uuid_1.v4)();
132
+ // Store this connection
133
+ this.wsConnections.set(wsConnectionId, {
134
+ clientSocket: ws,
135
+ id: wsConnectionId,
136
+ path: remainingPath,
137
+ headers: req.headers
138
+ });
139
+ // Notify tunnel client about the new WebSocket connection
140
+ tunnel.ws.send(JSON.stringify({
141
+ type: "websocket_connection",
142
+ id: wsConnectionId,
143
+ path: remainingPath,
144
+ headers: req.headers
145
+ }));
146
+ // Handle messages from the client
147
+ ws.on("message", (data) => {
148
+ tunnel.ws.send(JSON.stringify({
149
+ type: "websocket",
150
+ id: wsConnectionId,
151
+ event: "message",
152
+ data: data.toString('base64')
153
+ }));
154
+ });
155
+ // Handle client disconnection
156
+ ws.on("close", () => {
157
+ tunnel.ws.send(JSON.stringify({
158
+ type: "websocket",
159
+ id: wsConnectionId,
160
+ event: "close"
161
+ }));
162
+ this.wsConnections.delete(wsConnectionId);
163
+ });
164
+ // Handle errors
165
+ ws.on("error", (error) => {
166
+ tunnel.ws.send(JSON.stringify({
167
+ type: "websocket",
168
+ id: wsConnectionId,
169
+ event: "error",
170
+ data: error.message
171
+ }));
172
+ });
173
+ }
174
+ handleStartMessage(ws, data) {
175
+ const { apiKey, tunnelId } = data;
176
+ // Validate API key format (sk_agent_{agentId}_{orgId}_{secret})
177
+ if (!apiKey || !apiKey.startsWith('sk_agent_')) {
178
+ ws.close(1008, "Invalid API key");
179
+ return;
180
+ }
181
+ // TODO: Validate API key with platform in production
182
+ // For now, accept any properly formatted API key (dev mode)
183
+ try {
184
+ // If tunnel already exists, remove old one
99
185
  if (this.tunnels.has(tunnelId)) {
100
- console.log(`Tunnel ${tunnelId} still active`);
186
+ console.log(`Tunnel ${tunnelId} already exists, replacing it`);
187
+ const oldTunnel = this.tunnels.get(tunnelId);
188
+ if (oldTunnel && oldTunnel.ws !== ws) {
189
+ oldTunnel.ws.close(1000, "Replaced by new connection");
190
+ }
101
191
  }
102
- else {
103
- console.log(`Tunnel ${tunnelId} not found in periodic check`);
192
+ this.tunnels.set(tunnelId, { id: tunnelId, ws });
193
+ console.log(`Tunnel added: ${tunnelId}`);
194
+ console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
195
+ // Save tunnel ID on the WebSocket object for easy lookup on close
196
+ ws.tunnelId = tunnelId;
197
+ // Add a periodic check to ensure the tunnel is still in the map
198
+ const intervalId = setInterval(() => {
199
+ if (this.tunnels.has(tunnelId)) {
200
+ const tunnel = this.tunnels.get(tunnelId);
201
+ if (tunnel && tunnel.ws === ws && ws.readyState === ws_1.default.OPEN) {
202
+ console.log(`Tunnel ${tunnelId} still active`);
203
+ // Send a ping to keep the connection alive
204
+ try {
205
+ ws.ping();
206
+ }
207
+ catch (error) {
208
+ console.error(`Error sending ping to tunnel ${tunnelId}:`, error);
209
+ clearInterval(intervalId);
210
+ }
211
+ }
212
+ else {
213
+ console.log(`Tunnel ${tunnelId} exists but WebSocket state is invalid, cleaning up`);
214
+ clearInterval(intervalId);
215
+ if (tunnel && tunnel.ws === ws) {
216
+ this.tunnels.delete(tunnelId);
217
+ }
218
+ }
219
+ }
220
+ else {
221
+ console.log(`Tunnel ${tunnelId} not found in periodic check`);
222
+ clearInterval(intervalId);
223
+ }
224
+ }, 5000); // Check every 5 seconds
225
+ // Store the interval ID on the WebSocket object so we can clean it up
226
+ ws.keepAliveInterval = intervalId;
227
+ ws.on("close", () => {
228
+ console.log(`WebSocket for tunnel ${tunnelId} closed, clearing interval`);
104
229
  clearInterval(intervalId);
230
+ });
231
+ let tunnelUrl = `${this.hostname}`;
232
+ if (process.env.SKIP_PORT !== "true") {
233
+ tunnelUrl += `:${this.port}`;
105
234
  }
106
- }, 5000); // Check every 5 seconds
107
- ws.on("close", () => {
108
- clearInterval(intervalId);
109
- });
110
- let tunnelUrl = `${this.hostname}`;
111
- if (process.env.SKIP_PORT !== "true") {
112
- tunnelUrl += `:${this.port}`;
235
+ tunnelUrl += `/${tunnelId}`;
236
+ ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
237
+ console.log(`New tunnel created: ${tunnelUrl}`);
238
+ }
239
+ catch (error) {
240
+ console.error(`Error in handleStartMessage for tunnel ${tunnelId}:`, error);
241
+ ws.close(1011, "Internal server error");
113
242
  }
114
- tunnelUrl += `/${tunnelId}`;
115
- ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
116
- console.log(`New tunnel created: ${tunnelUrl}`);
117
243
  }
118
244
  handleResponseMessage(data) {
119
245
  const pendingRequest = this.pendingRequests.get(data.requestId);
@@ -133,8 +259,61 @@ class DainTunnelServer {
133
259
  this.pendingRequests.delete(data.requestId);
134
260
  }
135
261
  }
136
- async handleHttpRequest(req, res) {
262
+ handleSSEMessage(data) {
263
+ const connection = this.sseConnections.get(data.id);
264
+ if (!connection) {
265
+ console.log(`SSE connection not found: ${data.id}`);
266
+ return;
267
+ }
268
+ const { res } = connection;
269
+ if (data.event) {
270
+ res.write(`event: ${data.event}\n`);
271
+ }
272
+ // Split data by newlines and send each line
273
+ const dataLines = data.data.split('\n');
274
+ for (const line of dataLines) {
275
+ res.write(`data: ${line}\n`);
276
+ }
277
+ res.write('\n');
278
+ // Check if this is a "close" event
279
+ if (data.event === 'close') {
280
+ res.end();
281
+ this.sseConnections.delete(data.id);
282
+ }
283
+ }
284
+ handleWebSocketMessage(data) {
285
+ const connection = this.wsConnections.get(data.id);
286
+ if (!connection) {
287
+ console.log(`WebSocket connection not found: ${data.id}`);
288
+ return;
289
+ }
290
+ const { clientSocket } = connection;
291
+ if (data.event === 'message' && data.data) {
292
+ const messageData = Buffer.from(data.data, 'base64');
293
+ if (clientSocket.readyState === ws_1.default.OPEN) {
294
+ clientSocket.send(messageData);
295
+ }
296
+ }
297
+ else if (data.event === 'close') {
298
+ if (clientSocket.readyState === ws_1.default.OPEN) {
299
+ clientSocket.close();
300
+ }
301
+ this.wsConnections.delete(data.id);
302
+ }
303
+ else if (data.event === 'error' && data.data) {
304
+ if (clientSocket.readyState === ws_1.default.OPEN) {
305
+ clientSocket.close(1011, data.data);
306
+ }
307
+ this.wsConnections.delete(data.id);
308
+ }
309
+ }
310
+ async handleRequest(req, res) {
137
311
  const tunnelId = req.params.tunnelId;
312
+ // Check for upgraded connections (WebSockets) - these are handled by the WebSocket server
313
+ if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') {
314
+ // This is handled by the WebSocket server now
315
+ return;
316
+ }
138
317
  let tunnel;
139
318
  let retries = 3;
140
319
  while (retries > 0 && !tunnel) {
@@ -150,6 +329,11 @@ class DainTunnelServer {
150
329
  console.log(`Tunnel not found after retries: ${tunnelId}`);
151
330
  return res.status(404).send("Tunnel not found");
152
331
  }
332
+ // Check for SSE request
333
+ if (req.headers.accept && req.headers.accept.includes('text/event-stream')) {
334
+ return this.handleSSERequest(req, res, tunnelId, tunnel);
335
+ }
336
+ // Handle regular HTTP request
153
337
  const requestId = (0, uuid_1.v4)();
154
338
  const startTime = Date.now();
155
339
  this.pendingRequests.set(requestId, { res, startTime });
@@ -166,29 +350,102 @@ class DainTunnelServer {
166
350
  tunnel.ws.send(JSON.stringify(requestMessage));
167
351
  console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
168
352
  }
353
+ handleSSERequest(req, res, tunnelId, tunnel) {
354
+ // Setup SSE connection
355
+ const sseId = (0, uuid_1.v4)();
356
+ res.writeHead(200, {
357
+ 'Content-Type': 'text/event-stream',
358
+ 'Cache-Control': 'no-cache',
359
+ 'Connection': 'keep-alive'
360
+ });
361
+ // Send initial connection event
362
+ res.write('\n');
363
+ // Store the SSE connection
364
+ this.sseConnections.set(sseId, {
365
+ req,
366
+ res,
367
+ id: sseId,
368
+ tunnelId
369
+ });
370
+ // Notify the tunnel client about the new SSE connection
371
+ tunnel.ws.send(JSON.stringify({
372
+ type: "sse_connection",
373
+ id: sseId,
374
+ path: req.url,
375
+ headers: req.headers
376
+ }));
377
+ // Handle client disconnect
378
+ req.on('close', () => {
379
+ tunnel.ws.send(JSON.stringify({
380
+ type: "sse_close",
381
+ id: sseId
382
+ }));
383
+ this.sseConnections.delete(sseId);
384
+ });
385
+ }
169
386
  removeTunnel(ws) {
170
- let removedTunnelId;
171
- for (const [id, tunnel] of this.tunnels.entries()) {
172
- if (tunnel.ws === ws) {
173
- this.tunnels.delete(id);
174
- removedTunnelId = id;
175
- console.log(`Tunnel removed: ${id}`);
176
- break;
387
+ try {
388
+ // Clear any interval timer
389
+ if (ws.keepAliveInterval) {
390
+ clearInterval(ws.keepAliveInterval);
177
391
  }
178
- }
179
- if (removedTunnelId) {
180
- console.log(`Tunnel ${removedTunnelId} removed. Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
181
- }
182
- else {
183
- console.log(`No tunnel found to remove for the closed WebSocket connection`);
184
- }
185
- // Also remove any pending challenges for this WebSocket
186
- for (const [challenge, challengeObj] of this.challenges.entries()) {
187
- if (challengeObj.ws === ws) {
188
- this.challenges.delete(challenge);
189
- break;
392
+ // If we saved tunnelId on the WebSocket object, use it for faster lookup
393
+ const tunnelId = ws.tunnelId;
394
+ let removedTunnelId = tunnelId;
395
+ if (tunnelId && this.tunnels.has(tunnelId)) {
396
+ const tunnel = this.tunnels.get(tunnelId);
397
+ if (tunnel && tunnel.ws === ws) {
398
+ this.tunnels.delete(tunnelId);
399
+ console.log(`Tunnel removed using stored ID: ${tunnelId}`);
400
+ }
401
+ }
402
+ else {
403
+ // Fall back to iterating through all tunnels
404
+ removedTunnelId = undefined;
405
+ for (const [id, tunnel] of this.tunnels.entries()) {
406
+ if (tunnel.ws === ws) {
407
+ this.tunnels.delete(id);
408
+ removedTunnelId = id;
409
+ console.log(`Tunnel removed: ${id}`);
410
+ break;
411
+ }
412
+ }
413
+ }
414
+ if (removedTunnelId) {
415
+ console.log(`Tunnel ${removedTunnelId} removed. Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
416
+ // Close all SSE connections associated with this tunnel
417
+ for (const [sseId, sseConnection] of this.sseConnections.entries()) {
418
+ if (sseConnection.tunnelId === removedTunnelId) {
419
+ try {
420
+ sseConnection.res.end();
421
+ }
422
+ catch (error) {
423
+ console.error(`Error closing SSE connection ${sseId}:`, error);
424
+ }
425
+ this.sseConnections.delete(sseId);
426
+ }
427
+ }
428
+ // Close all WebSocket connections associated with this tunnel
429
+ for (const [wsId, wsConnection] of this.wsConnections.entries()) {
430
+ const wsPath = wsConnection.path;
431
+ if (wsPath.startsWith(`/${removedTunnelId}/`)) {
432
+ try {
433
+ wsConnection.clientSocket.close(1001, "Tunnel closed");
434
+ }
435
+ catch (error) {
436
+ console.error(`Error closing WebSocket connection ${wsId}:`, error);
437
+ }
438
+ this.wsConnections.delete(wsId);
439
+ }
440
+ }
441
+ }
442
+ else {
443
+ console.log(`No tunnel found to remove for the closed WebSocket connection`);
190
444
  }
191
445
  }
446
+ catch (error) {
447
+ console.error("Error in removeTunnel:", error);
448
+ }
192
449
  }
193
450
  async start() {
194
451
  return new Promise((resolve) => {
@@ -200,11 +457,39 @@ class DainTunnelServer {
200
457
  }
201
458
  async stop() {
202
459
  return new Promise((resolve) => {
203
- this.wss.close(() => {
204
- this.server.close(() => {
205
- resolve();
460
+ try {
461
+ // Close all SSE connections
462
+ for (const [sseId, sseConnection] of this.sseConnections.entries()) {
463
+ try {
464
+ sseConnection.res.end();
465
+ }
466
+ catch (error) {
467
+ console.error(`Error closing SSE connection ${sseId}:`, error);
468
+ }
469
+ this.sseConnections.delete(sseId);
470
+ }
471
+ // Close all WebSocket connections
472
+ for (const [wsId, wsConnection] of this.wsConnections.entries()) {
473
+ try {
474
+ wsConnection.clientSocket.close(1001, "Server shutting down");
475
+ }
476
+ catch (error) {
477
+ console.error(`Error closing WebSocket connection ${wsId}:`, error);
478
+ }
479
+ this.wsConnections.delete(wsId);
480
+ }
481
+ // Close the WebSocket server
482
+ this.wss.close(() => {
483
+ // Close the HTTP server
484
+ this.server.close(() => {
485
+ resolve();
486
+ });
206
487
  });
207
- });
488
+ }
489
+ catch (error) {
490
+ console.error('Error during server shutdown:', error);
491
+ resolve(); // Resolve anyway to prevent hanging
492
+ }
208
493
  });
209
494
  }
210
495
  }
@@ -13,6 +13,7 @@ const server = new index_1.default(hostname, port);
13
13
  server.start()
14
14
  .then(() => {
15
15
  console.log(`DainTunnel Server started on http://${hostname}:${port}`);
16
+ console.log(`Supports HTTP, WebSockets, and Server-Sent Events (SSE)`);
16
17
  })
17
18
  .catch((error) => {
18
19
  console.error('Failed to start DainTunnel Server:', error);
package/package.json CHANGED
@@ -1,13 +1,12 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "private": false,
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
10
-
11
10
  "scripts": {
12
11
  "build": "tsc",
13
12
  "build:types": "tsc --emitDeclarationOnly",
@@ -20,11 +19,11 @@
20
19
  "author": "Ryan",
21
20
  "license": "ISC",
22
21
  "dependencies": {
23
- "@dainprotocol/service-sdk": "^1.0.11",
22
+ "@dainprotocol/service-sdk": "^1.2.5",
24
23
  "@types/body-parser": "^1.19.5",
25
24
  "@types/cors": "^2.8.17",
25
+ "@types/eventsource": "^3.0.0",
26
26
  "@types/express": "^4.17.21",
27
- "@types/jest": "^29.5.12",
28
27
  "@types/node": "^22.5.4",
29
28
  "@types/uuid": "^10.0.0",
30
29
  "@types/ws": "^8.5.12",
@@ -32,21 +31,26 @@
32
31
  "bs58": "^6.0.0",
33
32
  "cors": "^2.8.5",
34
33
  "dotenv": "^16.4.5",
34
+ "eventsource": "^3.0.6",
35
35
  "express": "^4.19.2",
36
36
  "fetch-mock": "^11.1.3",
37
- "jest": "^29.7.0",
38
- "ts-jest": "^29.2.5",
39
37
  "ts-node": "^10.9.2",
40
- "typescript": "^5.5.4",
41
38
  "uuid": "^10.0.0",
42
39
  "ws": "^8.18.0"
43
40
  },
41
+ "devDependencies": {
42
+ "@types/express": "^4.17.21",
43
+ "@types/jest": "^29.5.12",
44
+ "jest": "^29.7.0",
45
+ "ts-jest": "^29.1.5",
46
+ "typescript": "^5.0.0"
47
+ },
44
48
  "files": [
45
49
  "dist",
46
50
  "README.md"
47
51
  ],
48
52
  "engines": {
49
- "node": "20.7.0"
53
+ "node": ">=20.7.0"
50
54
  },
51
55
  "exports": {
52
56
  ".": "./dist/index.js",