@dainprotocol/tunnel 1.0.1 → 1.0.3
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 +5 -2
- package/dist/client/index.js +49 -7
- package/dist/server/index.d.ts +2 -1
- package/dist/server/index.js +120 -33
- package/dist/server/start.js +5 -2
- package/package.json +14 -1
package/dist/client/index.d.ts
CHANGED
|
@@ -7,9 +7,12 @@ declare class DainTunnel extends EventEmitter {
|
|
|
7
7
|
private reconnectAttempts;
|
|
8
8
|
private maxReconnectAttempts;
|
|
9
9
|
private reconnectDelay;
|
|
10
|
-
|
|
10
|
+
private auth;
|
|
11
|
+
private tunnelId;
|
|
12
|
+
constructor(serverUrl: string, apiKey: string);
|
|
11
13
|
start(port: number): Promise<string>;
|
|
12
14
|
private connect;
|
|
15
|
+
private requestChallenge;
|
|
13
16
|
private handleMessage;
|
|
14
17
|
private handleRequest;
|
|
15
18
|
private forwardRequest;
|
|
@@ -19,6 +22,6 @@ declare class DainTunnel extends EventEmitter {
|
|
|
19
22
|
}
|
|
20
23
|
export { DainTunnel };
|
|
21
24
|
declare const _default: {
|
|
22
|
-
createTunnel: (serverUrl: string) => DainTunnel;
|
|
25
|
+
createTunnel: (serverUrl: string, apiKey: string) => DainTunnel;
|
|
23
26
|
};
|
|
24
27
|
export default _default;
|
package/dist/client/index.js
CHANGED
|
@@ -7,8 +7,10 @@ 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
12
|
class DainTunnel extends events_1.EventEmitter {
|
|
11
|
-
constructor(serverUrl) {
|
|
13
|
+
constructor(serverUrl, apiKey) {
|
|
12
14
|
super();
|
|
13
15
|
this.serverUrl = serverUrl;
|
|
14
16
|
this.ws = null;
|
|
@@ -17,6 +19,8 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
17
19
|
this.reconnectAttempts = 0;
|
|
18
20
|
this.maxReconnectAttempts = 5;
|
|
19
21
|
this.reconnectDelay = 5000;
|
|
22
|
+
this.auth = new client_1.DainClientAuth({ apiKey });
|
|
23
|
+
this.tunnelId = bs58_1.default.encode(this.auth.getPublicKey());
|
|
20
24
|
}
|
|
21
25
|
async start(port) {
|
|
22
26
|
this.port = port;
|
|
@@ -25,9 +29,17 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
25
29
|
async connect() {
|
|
26
30
|
return new Promise((resolve, reject) => {
|
|
27
31
|
this.ws = new ws_1.default(this.serverUrl);
|
|
28
|
-
this.ws.on("open", () => {
|
|
32
|
+
this.ws.on("open", async () => {
|
|
29
33
|
this.reconnectAttempts = 0;
|
|
30
|
-
|
|
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
|
|
42
|
+
});
|
|
31
43
|
this.emit("connected");
|
|
32
44
|
});
|
|
33
45
|
this.ws.on("message", (data) => {
|
|
@@ -51,6 +63,32 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
51
63
|
}, 5000);
|
|
52
64
|
});
|
|
53
65
|
}
|
|
66
|
+
async requestChallenge() {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
if (!this.ws) {
|
|
69
|
+
reject(new Error("WebSocket is not connected"));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
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
|
+
});
|
|
91
|
+
}
|
|
54
92
|
handleMessage(message, resolve) {
|
|
55
93
|
switch (message.type) {
|
|
56
94
|
case "tunnelUrl":
|
|
@@ -75,10 +113,14 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
75
113
|
}
|
|
76
114
|
forwardRequest(request) {
|
|
77
115
|
return new Promise((resolve, reject) => {
|
|
78
|
-
const
|
|
116
|
+
const options = {
|
|
117
|
+
hostname: 'localhost',
|
|
118
|
+
port: this.port,
|
|
119
|
+
path: request.path,
|
|
79
120
|
method: request.method,
|
|
80
121
|
headers: request.headers,
|
|
81
|
-
}
|
|
122
|
+
};
|
|
123
|
+
const req = http_1.default.request(options, (res) => {
|
|
82
124
|
let body = Buffer.from([]);
|
|
83
125
|
res.on('data', (chunk) => {
|
|
84
126
|
body = Buffer.concat([body, chunk]);
|
|
@@ -97,7 +139,7 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
97
139
|
});
|
|
98
140
|
});
|
|
99
141
|
req.on('error', reject);
|
|
100
|
-
if (request.body) {
|
|
142
|
+
if (request.body && request.method !== 'GET') {
|
|
101
143
|
req.write(Buffer.from(request.body, 'base64'));
|
|
102
144
|
}
|
|
103
145
|
req.end();
|
|
@@ -129,5 +171,5 @@ class DainTunnel extends events_1.EventEmitter {
|
|
|
129
171
|
}
|
|
130
172
|
exports.DainTunnel = DainTunnel;
|
|
131
173
|
exports.default = {
|
|
132
|
-
createTunnel: (serverUrl) => new DainTunnel(serverUrl),
|
|
174
|
+
createTunnel: (serverUrl, apiKey) => new DainTunnel(serverUrl, apiKey),
|
|
133
175
|
};
|
package/dist/server/index.d.ts
CHANGED
|
@@ -6,13 +6,14 @@ declare class DainTunnelServer {
|
|
|
6
6
|
private wss;
|
|
7
7
|
private tunnels;
|
|
8
8
|
private pendingRequests;
|
|
9
|
+
private challenges;
|
|
9
10
|
constructor(hostname: string, port: number);
|
|
10
11
|
private setupExpressRoutes;
|
|
11
12
|
private setupWebSocketServer;
|
|
13
|
+
private handleChallengeRequest;
|
|
12
14
|
private handleStartMessage;
|
|
13
15
|
private handleResponseMessage;
|
|
14
16
|
private handleHttpRequest;
|
|
15
|
-
private getRequestBody;
|
|
16
17
|
private removeTunnel;
|
|
17
18
|
start(): Promise<void>;
|
|
18
19
|
stop(): Promise<void>;
|
package/dist/server/index.js
CHANGED
|
@@ -7,43 +7,112 @@ const express_1 = __importDefault(require("express"));
|
|
|
7
7
|
const http_1 = __importDefault(require("http"));
|
|
8
8
|
const ws_1 = __importDefault(require("ws"));
|
|
9
9
|
const uuid_1 = require("uuid");
|
|
10
|
+
const body_parser_1 = __importDefault(require("body-parser"));
|
|
11
|
+
const cors_1 = __importDefault(require("cors")); // Add this import
|
|
12
|
+
const client_1 = require("@dainprotocol/service-sdk/client");
|
|
13
|
+
const bs58_1 = __importDefault(require("bs58"));
|
|
10
14
|
class DainTunnelServer {
|
|
11
15
|
constructor(hostname, port) {
|
|
12
16
|
this.hostname = hostname;
|
|
13
17
|
this.port = port;
|
|
14
18
|
this.tunnels = new Map();
|
|
15
19
|
this.pendingRequests = new Map();
|
|
20
|
+
this.challenges = new Map();
|
|
16
21
|
this.app = (0, express_1.default)();
|
|
17
22
|
this.server = http_1.default.createServer(this.app);
|
|
18
23
|
this.wss = new ws_1.default.Server({ server: this.server });
|
|
24
|
+
// Use cors middleware
|
|
25
|
+
this.app.use((0, cors_1.default)());
|
|
26
|
+
// Update CORS middleware
|
|
27
|
+
this.app.use((req, res, next) => {
|
|
28
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
29
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH, OPTIONS");
|
|
30
|
+
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, Content-Type, Authorization, Accept, Origin, X-Requested-With');
|
|
31
|
+
if (req.method === "OPTIONS") {
|
|
32
|
+
return res.sendStatus(200);
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
});
|
|
36
|
+
// Add body-parser middleware
|
|
37
|
+
this.app.use(body_parser_1.default.raw({ type: "*/*", limit: "100mb" }));
|
|
19
38
|
this.setupExpressRoutes();
|
|
20
39
|
this.setupWebSocketServer();
|
|
21
40
|
}
|
|
22
41
|
setupExpressRoutes() {
|
|
23
|
-
this.app.use(
|
|
42
|
+
this.app.use("/:tunnelId", this.handleHttpRequest.bind(this));
|
|
24
43
|
}
|
|
25
44
|
setupWebSocketServer() {
|
|
26
|
-
this.wss.on(
|
|
27
|
-
console.log(
|
|
28
|
-
ws.on(
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
this.wss.on("connection", (ws) => {
|
|
46
|
+
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
|
+
}
|
|
32
60
|
}
|
|
33
|
-
|
|
34
|
-
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error("Error processing message:", error);
|
|
63
|
+
ws.close(1008, "Invalid message");
|
|
35
64
|
}
|
|
36
65
|
});
|
|
37
|
-
ws.on(
|
|
66
|
+
ws.on("close", () => {
|
|
67
|
+
console.log("WebSocket connection closed");
|
|
38
68
|
this.removeTunnel(ws);
|
|
39
69
|
});
|
|
70
|
+
ws.on("error", (error) => {
|
|
71
|
+
console.error("WebSocket error:", error);
|
|
72
|
+
});
|
|
40
73
|
});
|
|
41
74
|
}
|
|
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 }));
|
|
80
|
+
}
|
|
42
81
|
handleStartMessage(ws, data) {
|
|
43
|
-
const tunnelId =
|
|
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");
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
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");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
44
94
|
this.tunnels.set(tunnelId, { id: tunnelId, ws });
|
|
45
|
-
|
|
46
|
-
|
|
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(() => {
|
|
99
|
+
if (this.tunnels.has(tunnelId)) {
|
|
100
|
+
console.log(`Tunnel ${tunnelId} still active`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
console.log(`Tunnel ${tunnelId} not found in periodic check`);
|
|
104
|
+
clearInterval(intervalId);
|
|
105
|
+
}
|
|
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}`;
|
|
113
|
+
}
|
|
114
|
+
tunnelUrl += `/${tunnelId}`;
|
|
115
|
+
ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
|
|
47
116
|
console.log(`New tunnel created: ${tunnelUrl}`);
|
|
48
117
|
}
|
|
49
118
|
handleResponseMessage(data) {
|
|
@@ -52,12 +121,13 @@ class DainTunnelServer {
|
|
|
52
121
|
const { res, startTime } = pendingRequest;
|
|
53
122
|
const endTime = Date.now();
|
|
54
123
|
const headers = { ...data.headers };
|
|
55
|
-
delete headers[
|
|
56
|
-
delete headers[
|
|
57
|
-
const bodyBuffer = Buffer.from(data.body,
|
|
58
|
-
res
|
|
124
|
+
delete headers["transfer-encoding"];
|
|
125
|
+
delete headers["content-length"];
|
|
126
|
+
const bodyBuffer = Buffer.from(data.body, "base64");
|
|
127
|
+
res
|
|
128
|
+
.status(data.status)
|
|
59
129
|
.set(headers)
|
|
60
|
-
.set(
|
|
130
|
+
.set("Content-Length", bodyBuffer.length.toString())
|
|
61
131
|
.send(bodyBuffer);
|
|
62
132
|
console.log(`Request handled: ${data.requestId}, Duration: ${endTime - startTime}ms`);
|
|
63
133
|
this.pendingRequests.delete(data.requestId);
|
|
@@ -65,43 +135,60 @@ class DainTunnelServer {
|
|
|
65
135
|
}
|
|
66
136
|
async handleHttpRequest(req, res) {
|
|
67
137
|
const tunnelId = req.params.tunnelId;
|
|
68
|
-
|
|
138
|
+
let tunnel;
|
|
139
|
+
let retries = 3;
|
|
140
|
+
while (retries > 0 && !tunnel) {
|
|
141
|
+
tunnel = this.tunnels.get(tunnelId);
|
|
142
|
+
if (!tunnel) {
|
|
143
|
+
console.log(`Tunnel not found: ${tunnelId}, retrying... (${retries} attempts left)`);
|
|
144
|
+
console.log(`Current tunnels: ${Array.from(this.tunnels.keys()).join(', ')}`);
|
|
145
|
+
await new Promise(resolve => setTimeout(resolve, 100)); // Wait 100ms before retrying
|
|
146
|
+
retries--;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
69
149
|
if (!tunnel) {
|
|
70
|
-
|
|
150
|
+
console.log(`Tunnel not found after retries: ${tunnelId}`);
|
|
151
|
+
return res.status(404).send("Tunnel not found");
|
|
71
152
|
}
|
|
72
153
|
const requestId = (0, uuid_1.v4)();
|
|
73
154
|
const startTime = Date.now();
|
|
74
155
|
this.pendingRequests.set(requestId, { res, startTime });
|
|
75
156
|
const requestMessage = {
|
|
76
|
-
type:
|
|
157
|
+
type: "request",
|
|
77
158
|
id: requestId,
|
|
78
159
|
method: req.method,
|
|
79
160
|
path: req.url,
|
|
80
161
|
headers: req.headers,
|
|
81
|
-
body:
|
|
162
|
+
body: req.method !== "GET" && req.body
|
|
163
|
+
? req.body.toString("base64")
|
|
164
|
+
: undefined,
|
|
82
165
|
};
|
|
83
166
|
tunnel.ws.send(JSON.stringify(requestMessage));
|
|
84
167
|
console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
|
|
85
168
|
}
|
|
86
|
-
getRequestBody(req) {
|
|
87
|
-
return new Promise((resolve) => {
|
|
88
|
-
let body = '';
|
|
89
|
-
req.on('data', chunk => {
|
|
90
|
-
body += chunk.toString();
|
|
91
|
-
});
|
|
92
|
-
req.on('end', () => {
|
|
93
|
-
resolve(body);
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
169
|
removeTunnel(ws) {
|
|
170
|
+
let removedTunnelId;
|
|
98
171
|
for (const [id, tunnel] of this.tunnels.entries()) {
|
|
99
172
|
if (tunnel.ws === ws) {
|
|
100
173
|
this.tunnels.delete(id);
|
|
174
|
+
removedTunnelId = id;
|
|
101
175
|
console.log(`Tunnel removed: ${id}`);
|
|
102
176
|
break;
|
|
103
177
|
}
|
|
104
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;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
105
192
|
}
|
|
106
193
|
async start() {
|
|
107
194
|
return new Promise((resolve) => {
|
package/dist/server/start.js
CHANGED
|
@@ -4,12 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const index_1 = __importDefault(require("./index"));
|
|
7
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
8
|
+
// Load environment variables from .env file
|
|
9
|
+
dotenv_1.default.config();
|
|
7
10
|
const port = parseInt(process.env.PORT || '3000', 10);
|
|
8
|
-
const hostname = process.env.HOSTNAME || '
|
|
11
|
+
const hostname = process.env.HOSTNAME || 'localhost';
|
|
9
12
|
const server = new index_1.default(hostname, port);
|
|
10
13
|
server.start()
|
|
11
14
|
.then(() => {
|
|
12
|
-
console.log(`DainTunnel Server started on
|
|
15
|
+
console.log(`DainTunnel Server started on http://${hostname}:${port}`);
|
|
13
16
|
})
|
|
14
17
|
.catch((error) => {
|
|
15
18
|
console.error('Failed to start DainTunnel Server:', error);
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dainprotocol/tunnel",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
|
+
"private": false,
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
|
|
6
11
|
"scripts": {
|
|
7
12
|
"build": "tsc",
|
|
8
13
|
"build:types": "tsc --emitDeclarationOnly",
|
|
@@ -15,15 +20,23 @@
|
|
|
15
20
|
"author": "Ryan",
|
|
16
21
|
"license": "ISC",
|
|
17
22
|
"dependencies": {
|
|
23
|
+
"@dainprotocol/service-sdk": "^1.0.11",
|
|
24
|
+
"@types/body-parser": "^1.19.5",
|
|
25
|
+
"@types/cors": "^2.8.17",
|
|
18
26
|
"@types/express": "^4.17.21",
|
|
19
27
|
"@types/jest": "^29.5.12",
|
|
20
28
|
"@types/node": "^22.5.4",
|
|
21
29
|
"@types/uuid": "^10.0.0",
|
|
22
30
|
"@types/ws": "^8.5.12",
|
|
31
|
+
"body-parser": "^1.20.2",
|
|
32
|
+
"bs58": "^6.0.0",
|
|
33
|
+
"cors": "^2.8.5",
|
|
34
|
+
"dotenv": "^16.4.5",
|
|
23
35
|
"express": "^4.19.2",
|
|
24
36
|
"fetch-mock": "^11.1.3",
|
|
25
37
|
"jest": "^29.7.0",
|
|
26
38
|
"ts-jest": "^29.2.5",
|
|
39
|
+
"ts-node": "^10.9.2",
|
|
27
40
|
"typescript": "^5.5.4",
|
|
28
41
|
"uuid": "^10.0.0",
|
|
29
42
|
"ws": "^8.18.0"
|