@dainprotocol/tunnel 1.0.0 → 1.0.2

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,92 @@ 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
+ if (data.type === "challenge_request") {
51
+ this.handleChallengeRequest(ws);
52
+ }
53
+ else if (data.type === "start") {
54
+ this.handleStartMessage(ws, data);
55
+ }
56
+ else if (data.type === "response") {
57
+ this.handleResponseMessage(data);
58
+ }
32
59
  }
33
- else if (data.type === 'response') {
34
- this.handleResponseMessage(data);
60
+ catch (error) {
61
+ console.error("Error processing message:", error);
62
+ ws.close(1008, "Invalid message");
35
63
  }
36
64
  });
37
- ws.on('close', () => {
65
+ ws.on("close", () => {
38
66
  this.removeTunnel(ws);
39
67
  });
40
68
  });
41
69
  }
70
+ handleChallengeRequest(ws) {
71
+ const challenge = (0, uuid_1.v4)();
72
+ const challengeObj = { ws, challenge };
73
+ this.challenges.set(challenge, challengeObj);
74
+ ws.send(JSON.stringify({ type: "challenge", challenge }));
75
+ }
42
76
  handleStartMessage(ws, data) {
43
- const tunnelId = (0, uuid_1.v4)();
77
+ const { challenge, signature, tunnelId } = data;
78
+ const challengeObj = this.challenges.get(challenge);
79
+ if (!challengeObj || challengeObj.ws !== ws) {
80
+ ws.close(1008, "Invalid challenge");
81
+ return;
82
+ }
83
+ this.challenges.delete(challenge);
84
+ const publicKey = bs58_1.default.decode(tunnelId);
85
+ if (!client_1.DainClientAuth.verifyMessage(challenge, signature, publicKey)) {
86
+ ws.close(1008, "Invalid signature");
87
+ return;
88
+ }
44
89
  this.tunnels.set(tunnelId, { id: tunnelId, ws });
45
- const tunnelUrl = `${this.hostname}:${this.port}/${tunnelId}`;
46
- ws.send(JSON.stringify({ type: 'tunnelUrl', url: tunnelUrl }));
90
+ let tunnelUrl = `${this.hostname}`;
91
+ if (process.env.SKIP_PORT !== "true") {
92
+ tunnelUrl += `:${this.port}`;
93
+ }
94
+ tunnelUrl += `/${tunnelId}`;
95
+ ws.send(JSON.stringify({ type: "tunnelUrl", url: tunnelUrl }));
47
96
  console.log(`New tunnel created: ${tunnelUrl}`);
48
97
  }
49
98
  handleResponseMessage(data) {
@@ -52,12 +101,13 @@ class DainTunnelServer {
52
101
  const { res, startTime } = pendingRequest;
53
102
  const endTime = Date.now();
54
103
  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)
104
+ delete headers["transfer-encoding"];
105
+ delete headers["content-length"];
106
+ const bodyBuffer = Buffer.from(data.body, "base64");
107
+ res
108
+ .status(data.status)
59
109
  .set(headers)
60
- .set('Content-Length', bodyBuffer.length.toString())
110
+ .set("Content-Length", bodyBuffer.length.toString())
61
111
  .send(bodyBuffer);
62
112
  console.log(`Request handled: ${data.requestId}, Duration: ${endTime - startTime}ms`);
63
113
  this.pendingRequests.delete(data.requestId);
@@ -67,33 +117,25 @@ class DainTunnelServer {
67
117
  const tunnelId = req.params.tunnelId;
68
118
  const tunnel = this.tunnels.get(tunnelId);
69
119
  if (!tunnel) {
70
- return res.status(404).send('Tunnel not found');
120
+ console.log(`Tunnel not found: ${tunnelId}`);
121
+ return res.status(404).send("Tunnel not found");
71
122
  }
72
123
  const requestId = (0, uuid_1.v4)();
73
124
  const startTime = Date.now();
74
125
  this.pendingRequests.set(requestId, { res, startTime });
75
126
  const requestMessage = {
76
- type: 'request',
127
+ type: "request",
77
128
  id: requestId,
78
129
  method: req.method,
79
130
  path: req.url,
80
131
  headers: req.headers,
81
- body: await this.getRequestBody(req)
132
+ body: req.method !== "GET" && req.body
133
+ ? req.body.toString("base64")
134
+ : undefined,
82
135
  };
83
136
  tunnel.ws.send(JSON.stringify(requestMessage));
84
137
  console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
85
138
  }
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
139
  removeTunnel(ws) {
98
140
  for (const [id, tunnel] of this.tunnels.entries()) {
99
141
  if (tunnel.ws === ws) {
@@ -102,6 +144,13 @@ class DainTunnelServer {
102
144
  break;
103
145
  }
104
146
  }
147
+ // Also remove any pending challenges for this WebSocket
148
+ for (const [challenge, challengeObj] of this.challenges.entries()) {
149
+ if (challengeObj.ws === ws) {
150
+ this.challenges.delete(challenge);
151
+ break;
152
+ }
153
+ }
105
154
  }
106
155
  async start() {
107
156
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -15,23 +15,58 @@
15
15
  "author": "Ryan",
16
16
  "license": "ISC",
17
17
  "dependencies": {
18
+ "@dainprotocol/service-sdk": "^1.0.11",
19
+ "@types/body-parser": "^1.19.5",
20
+ "@types/cors": "^2.8.17",
18
21
  "@types/express": "^4.17.21",
19
22
  "@types/jest": "^29.5.12",
20
23
  "@types/node": "^22.5.4",
21
24
  "@types/uuid": "^10.0.0",
22
25
  "@types/ws": "^8.5.12",
26
+ "body-parser": "^1.20.2",
27
+ "bs58": "^6.0.0",
28
+ "cors": "^2.8.5",
29
+ "dotenv": "^16.4.5",
23
30
  "express": "^4.19.2",
24
31
  "fetch-mock": "^11.1.3",
25
32
  "jest": "^29.7.0",
26
33
  "ts-jest": "^29.2.5",
34
+ "ts-node": "^10.9.2",
27
35
  "typescript": "^5.5.4",
28
36
  "uuid": "^10.0.0",
29
37
  "ws": "^8.18.0"
30
- }, "files": [
38
+ },
39
+ "files": [
31
40
  "dist",
32
41
  "README.md"
33
42
  ],
34
43
  "engines": {
35
44
  "node": "20.7.0"
45
+ },
46
+ "exports": {
47
+ ".": "./dist/index.js",
48
+ "./client": "./dist/client/index.js",
49
+ "./server": "./dist/server/index.js",
50
+ "./client/*": "./dist/client/*.js",
51
+ "./server/*": "./dist/server/*.js"
52
+ },
53
+ "typesVersions": {
54
+ "*": {
55
+ ".": [
56
+ "./dist/index.d.ts"
57
+ ],
58
+ "client": [
59
+ "./dist/client/index.d.ts"
60
+ ],
61
+ "server": [
62
+ "./dist/server/index.d.ts"
63
+ ],
64
+ "client/*": [
65
+ "./dist/client/*.d.ts"
66
+ ],
67
+ "server/*": [
68
+ "./dist/server/*.d.ts"
69
+ ]
70
+ }
36
71
  }
37
72
  }