@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.
@@ -7,9 +7,12 @@ declare class DainTunnel extends EventEmitter {
7
7
  private reconnectAttempts;
8
8
  private maxReconnectAttempts;
9
9
  private reconnectDelay;
10
- constructor(serverUrl: string);
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;
@@ -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
- this.sendMessage({ type: "start", port: this.port });
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 req = http_1.default.request(`http://localhost:${this.port}${request.path}`, {
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
- }, (res) => {
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
  };
@@ -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>;
@@ -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('/:tunnelId', this.handleHttpRequest.bind(this));
42
+ this.app.use("/:tunnelId", this.handleHttpRequest.bind(this));
24
43
  }
25
44
  setupWebSocketServer() {
26
- this.wss.on('connection', (ws) => {
27
- console.log('New WebSocket connection');
28
- ws.on('message', (message) => {
29
- const data = JSON.parse(message);
30
- if (data.type === 'start') {
31
- this.handleStartMessage(ws, data);
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
- else if (data.type === 'response') {
34
- this.handleResponseMessage(data);
61
+ catch (error) {
62
+ console.error("Error processing message:", error);
63
+ ws.close(1008, "Invalid message");
35
64
  }
36
65
  });
37
- ws.on('close', () => {
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 = (0, uuid_1.v4)();
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
- const tunnelUrl = `${this.hostname}:${this.port}/${tunnelId}`;
46
- ws.send(JSON.stringify({ type: 'tunnelUrl', url: tunnelUrl }));
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['transfer-encoding'];
56
- delete headers['content-length'];
57
- const bodyBuffer = Buffer.from(data.body, 'base64');
58
- res.status(data.status)
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('Content-Length', bodyBuffer.length.toString())
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
- const tunnel = this.tunnels.get(tunnelId);
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
- return res.status(404).send('Tunnel not found');
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: 'request',
157
+ type: "request",
77
158
  id: requestId,
78
159
  method: req.method,
79
160
  path: req.url,
80
161
  headers: req.headers,
81
- body: await this.getRequestBody(req)
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) => {
@@ -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 || 'http://localhost';
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 ${hostname}:${port}`);
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.1",
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"