@dainprotocol/tunnel 1.0.7 → 1.1.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 +7 -0
- package/dist/client/index.js +61 -12
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.js +49 -7
- package/package.json +2 -2
package/dist/client/index.d.ts
CHANGED
|
@@ -9,11 +9,18 @@ declare class DainTunnel extends EventEmitter {
|
|
|
9
9
|
private reconnectDelay;
|
|
10
10
|
private apiKey;
|
|
11
11
|
private tunnelId;
|
|
12
|
+
private secret;
|
|
12
13
|
private webSocketClients;
|
|
13
14
|
private sseClients;
|
|
14
15
|
constructor(serverUrl: string, apiKey: string);
|
|
16
|
+
/**
|
|
17
|
+
* Sign a challenge using HMAC-SHA256
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
private signChallenge;
|
|
15
21
|
start(port: number): Promise<string>;
|
|
16
22
|
private connect;
|
|
23
|
+
private requestChallenge;
|
|
17
24
|
private handleMessage;
|
|
18
25
|
private handleRequest;
|
|
19
26
|
private handleWebSocketConnection;
|
package/dist/client/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const ws_1 = __importDefault(require("ws"));
|
|
|
8
8
|
const http_1 = __importDefault(require("http"));
|
|
9
9
|
const events_1 = require("events");
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
|
+
const auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
11
12
|
class DainTunnel extends events_1.EventEmitter {
|
|
12
13
|
constructor(serverUrl, apiKey) {
|
|
13
14
|
super();
|
|
@@ -20,10 +21,23 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
20
21
|
this.reconnectDelay = 5000;
|
|
21
22
|
this.webSocketClients = new Map();
|
|
22
23
|
this.sseClients = new Map();
|
|
24
|
+
// Parse API key to extract agentId and secret
|
|
25
|
+
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
26
|
+
if (!parsed) {
|
|
27
|
+
throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
|
|
28
|
+
}
|
|
23
29
|
this.apiKey = apiKey;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
this.tunnelId = parsed.agentId; // agentId is the tunnel identifier
|
|
31
|
+
this.secret = parsed.secret; // secret for HMAC signatures
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sign a challenge using HMAC-SHA256
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
signChallenge(challenge) {
|
|
38
|
+
return (0, crypto_1.createHmac)('sha256', this.secret)
|
|
39
|
+
.update(challenge)
|
|
40
|
+
.digest('hex');
|
|
27
41
|
}
|
|
28
42
|
async start(port) {
|
|
29
43
|
this.port = port;
|
|
@@ -34,17 +48,26 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
34
48
|
try {
|
|
35
49
|
console.log(`Connecting to WebSocket server: ${this.serverUrl}`);
|
|
36
50
|
this.ws = new ws_1.default(this.serverUrl);
|
|
37
|
-
this.ws.on("open", () => {
|
|
51
|
+
this.ws.on("open", async () => {
|
|
38
52
|
console.log('WebSocket connection opened');
|
|
39
53
|
this.reconnectAttempts = 0;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
try {
|
|
55
|
+
const challenge = await this.requestChallenge();
|
|
56
|
+
const signature = this.signChallenge(challenge);
|
|
57
|
+
this.sendMessage({
|
|
58
|
+
type: "start",
|
|
59
|
+
port: this.port,
|
|
60
|
+
challenge,
|
|
61
|
+
signature,
|
|
62
|
+
tunnelId: this.tunnelId,
|
|
63
|
+
apiKey: this.apiKey // Send API key for server validation
|
|
64
|
+
});
|
|
65
|
+
this.emit("connected");
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
console.error('Error during challenge-response:', err);
|
|
69
|
+
reject(err);
|
|
70
|
+
}
|
|
48
71
|
});
|
|
49
72
|
this.ws.on("message", (data) => {
|
|
50
73
|
try {
|
|
@@ -86,6 +109,32 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
86
109
|
}
|
|
87
110
|
});
|
|
88
111
|
}
|
|
112
|
+
async requestChallenge() {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
if (!this.ws) {
|
|
115
|
+
reject(new Error("WebSocket is not connected"));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
this.ws.send(JSON.stringify({ type: "challenge_request" }));
|
|
119
|
+
const challengeHandler = (message) => {
|
|
120
|
+
const data = JSON.parse(message);
|
|
121
|
+
if (data.type === "challenge") {
|
|
122
|
+
if (this.ws) {
|
|
123
|
+
this.ws.removeListener("message", challengeHandler);
|
|
124
|
+
}
|
|
125
|
+
resolve(data.challenge);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
this.ws.on("message", challengeHandler);
|
|
129
|
+
// Add a timeout for the challenge request
|
|
130
|
+
setTimeout(() => {
|
|
131
|
+
if (this.ws) {
|
|
132
|
+
this.ws.removeListener("message", challengeHandler);
|
|
133
|
+
}
|
|
134
|
+
reject(new Error("Challenge request timeout"));
|
|
135
|
+
}, 5000);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
89
138
|
handleMessage(message, resolve) {
|
|
90
139
|
switch (message.type) {
|
|
91
140
|
case "tunnelUrl":
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ declare class DainTunnelServer {
|
|
|
6
6
|
private wss;
|
|
7
7
|
private tunnels;
|
|
8
8
|
private pendingRequests;
|
|
9
|
+
private challenges;
|
|
9
10
|
private sseConnections;
|
|
10
11
|
private wsConnections;
|
|
11
12
|
constructor(hostname: string, port: number);
|
|
@@ -13,6 +14,7 @@ declare class DainTunnelServer {
|
|
|
13
14
|
private setupWebSocketServer;
|
|
14
15
|
private handleTunnelClientConnection;
|
|
15
16
|
private handleProxiedWebSocketConnection;
|
|
17
|
+
private handleChallengeRequest;
|
|
16
18
|
private handleStartMessage;
|
|
17
19
|
private handleResponseMessage;
|
|
18
20
|
private handleSSEMessage;
|
package/dist/server/index.js
CHANGED
|
@@ -9,6 +9,8 @@ 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 auth_1 = require("@dainprotocol/service-sdk/service/auth");
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
12
14
|
const url_1 = require("url");
|
|
13
15
|
class DainTunnelServer {
|
|
14
16
|
constructor(hostname, port) {
|
|
@@ -16,6 +18,7 @@ class DainTunnelServer {
|
|
|
16
18
|
this.port = port;
|
|
17
19
|
this.tunnels = new Map();
|
|
18
20
|
this.pendingRequests = new Map();
|
|
21
|
+
this.challenges = new Map();
|
|
19
22
|
this.sseConnections = new Map();
|
|
20
23
|
this.wsConnections = new Map();
|
|
21
24
|
this.app = (0, express_1.default)();
|
|
@@ -82,7 +85,10 @@ class DainTunnelServer {
|
|
|
82
85
|
try {
|
|
83
86
|
const data = JSON.parse(message);
|
|
84
87
|
console.log(`Received WebSocket message: ${data.type}`);
|
|
85
|
-
if (data.type === "
|
|
88
|
+
if (data.type === "challenge_request") {
|
|
89
|
+
this.handleChallengeRequest(ws);
|
|
90
|
+
}
|
|
91
|
+
else if (data.type === "start") {
|
|
86
92
|
this.handleStartMessage(ws, data);
|
|
87
93
|
}
|
|
88
94
|
else if (data.type === "response") {
|
|
@@ -171,16 +177,44 @@ class DainTunnelServer {
|
|
|
171
177
|
}));
|
|
172
178
|
});
|
|
173
179
|
}
|
|
180
|
+
handleChallengeRequest(ws) {
|
|
181
|
+
const challenge = (0, uuid_1.v4)();
|
|
182
|
+
const challengeObj = { ws, challenge };
|
|
183
|
+
this.challenges.set(challenge, challengeObj);
|
|
184
|
+
ws.send(JSON.stringify({ type: "challenge", challenge }));
|
|
185
|
+
}
|
|
174
186
|
handleStartMessage(ws, data) {
|
|
175
|
-
const {
|
|
176
|
-
|
|
177
|
-
if (!
|
|
178
|
-
ws.close(1008, "Invalid
|
|
187
|
+
const { challenge, signature, tunnelId, apiKey } = data;
|
|
188
|
+
const challengeObj = this.challenges.get(challenge);
|
|
189
|
+
if (!challengeObj || challengeObj.ws !== ws) {
|
|
190
|
+
ws.close(1008, "Invalid challenge");
|
|
179
191
|
return;
|
|
180
192
|
}
|
|
181
|
-
|
|
182
|
-
// For now, accept any properly formatted API key (dev mode)
|
|
193
|
+
this.challenges.delete(challenge);
|
|
183
194
|
try {
|
|
195
|
+
// Parse API key to get the secret for HMAC validation
|
|
196
|
+
if (!apiKey) {
|
|
197
|
+
ws.close(1008, "API key required");
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const parsed = (0, auth_1.parseAPIKey)(apiKey);
|
|
201
|
+
if (!parsed) {
|
|
202
|
+
ws.close(1008, "Invalid API key format");
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Validate HMAC signature
|
|
206
|
+
const expectedSignature = (0, crypto_1.createHmac)('sha256', parsed.secret)
|
|
207
|
+
.update(challenge)
|
|
208
|
+
.digest('hex');
|
|
209
|
+
if (expectedSignature !== signature) {
|
|
210
|
+
ws.close(1008, "Invalid signature");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
// Verify that tunnelId matches the agentId from the API key
|
|
214
|
+
if (tunnelId !== parsed.agentId) {
|
|
215
|
+
ws.close(1008, "Tunnel ID does not match API key");
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
184
218
|
// If tunnel already exists, remove old one
|
|
185
219
|
if (this.tunnels.has(tunnelId)) {
|
|
186
220
|
console.log(`Tunnel ${tunnelId} already exists, replacing it`);
|
|
@@ -442,6 +476,14 @@ class DainTunnelServer {
|
|
|
442
476
|
else {
|
|
443
477
|
console.log(`No tunnel found to remove for the closed WebSocket connection`);
|
|
444
478
|
}
|
|
479
|
+
// Also remove any pending challenges for this WebSocket
|
|
480
|
+
for (const [challenge, challengeObj] of this.challenges.entries()) {
|
|
481
|
+
if (challengeObj.ws === ws) {
|
|
482
|
+
this.challenges.delete(challenge);
|
|
483
|
+
console.log(`Challenge removed for closed WebSocket`);
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
445
487
|
}
|
|
446
488
|
catch (error) {
|
|
447
489
|
console.error("Error in removeTunnel:", error);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"private": false,
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"author": "Ryan",
|
|
20
20
|
"license": "ISC",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@dainprotocol/service-sdk": "^1.
|
|
22
|
+
"@dainprotocol/service-sdk": "^1.3.0",
|
|
23
23
|
"@types/body-parser": "^1.19.5",
|
|
24
24
|
"@types/cors": "^2.8.17",
|
|
25
25
|
"@types/eventsource": "^3.0.0",
|