@dainprotocol/tunnel 1.0.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.
@@ -0,0 +1,24 @@
1
+ import { EventEmitter } from "events";
2
+ declare class DainTunnel extends EventEmitter {
3
+ private serverUrl;
4
+ private ws;
5
+ private tunnelUrl;
6
+ private port;
7
+ private reconnectAttempts;
8
+ private maxReconnectAttempts;
9
+ private reconnectDelay;
10
+ constructor(serverUrl: string);
11
+ start(port: number): Promise<string>;
12
+ private connect;
13
+ private handleMessage;
14
+ private handleRequest;
15
+ private forwardRequest;
16
+ private sendMessage;
17
+ private attemptReconnect;
18
+ stop(): Promise<void>;
19
+ }
20
+ export { DainTunnel };
21
+ declare const _default: {
22
+ createTunnel: (serverUrl: string) => DainTunnel;
23
+ };
24
+ export default _default;
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DainTunnel = void 0;
7
+ const ws_1 = __importDefault(require("ws"));
8
+ const http_1 = __importDefault(require("http"));
9
+ const events_1 = require("events");
10
+ class DainTunnel extends events_1.EventEmitter {
11
+ constructor(serverUrl) {
12
+ super();
13
+ this.serverUrl = serverUrl;
14
+ this.ws = null;
15
+ this.tunnelUrl = null;
16
+ this.port = null;
17
+ this.reconnectAttempts = 0;
18
+ this.maxReconnectAttempts = 5;
19
+ this.reconnectDelay = 5000;
20
+ }
21
+ async start(port) {
22
+ this.port = port;
23
+ return this.connect();
24
+ }
25
+ async connect() {
26
+ return new Promise((resolve, reject) => {
27
+ this.ws = new ws_1.default(this.serverUrl);
28
+ this.ws.on("open", () => {
29
+ this.reconnectAttempts = 0;
30
+ this.sendMessage({ type: "start", port: this.port });
31
+ this.emit("connected");
32
+ });
33
+ this.ws.on("message", (data) => {
34
+ const message = JSON.parse(data);
35
+ this.handleMessage(message, resolve);
36
+ });
37
+ this.ws.on("close", () => {
38
+ this.emit("disconnected");
39
+ this.attemptReconnect();
40
+ });
41
+ this.ws.on("error", (error) => {
42
+ this.emit("error", error);
43
+ reject(error);
44
+ });
45
+ // Add a timeout to reject the promise if connection takes too long
46
+ setTimeout(() => {
47
+ var _a;
48
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) !== ws_1.default.OPEN) {
49
+ reject(new Error("Connection timeout"));
50
+ }
51
+ }, 5000);
52
+ });
53
+ }
54
+ handleMessage(message, resolve) {
55
+ switch (message.type) {
56
+ case "tunnelUrl":
57
+ this.tunnelUrl = message.url;
58
+ resolve(this.tunnelUrl);
59
+ this.emit("tunnel_created", this.tunnelUrl);
60
+ break;
61
+ case "request":
62
+ this.handleRequest(message);
63
+ break;
64
+ }
65
+ }
66
+ async handleRequest(request) {
67
+ try {
68
+ const response = await this.forwardRequest(request);
69
+ this.sendMessage(response);
70
+ this.emit("request_handled", { request, response });
71
+ }
72
+ catch (error) {
73
+ this.emit("request_error", { request, error });
74
+ }
75
+ }
76
+ forwardRequest(request) {
77
+ return new Promise((resolve, reject) => {
78
+ const req = http_1.default.request(`http://localhost:${this.port}${request.path}`, {
79
+ method: request.method,
80
+ headers: request.headers,
81
+ }, (res) => {
82
+ let body = Buffer.from([]);
83
+ res.on('data', (chunk) => {
84
+ body = Buffer.concat([body, chunk]);
85
+ });
86
+ res.on('end', () => {
87
+ const headers = { ...res.headers };
88
+ delete headers['transfer-encoding'];
89
+ delete headers['content-length'];
90
+ resolve({
91
+ type: 'response',
92
+ requestId: request.id,
93
+ status: res.statusCode,
94
+ headers,
95
+ body: body.toString('base64'),
96
+ });
97
+ });
98
+ });
99
+ req.on('error', reject);
100
+ if (request.body) {
101
+ req.write(Buffer.from(request.body, 'base64'));
102
+ }
103
+ req.end();
104
+ });
105
+ }
106
+ sendMessage(message) {
107
+ if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
108
+ this.ws.send(JSON.stringify(message));
109
+ }
110
+ }
111
+ attemptReconnect() {
112
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
113
+ this.reconnectAttempts++;
114
+ setTimeout(() => this.connect(), this.reconnectDelay);
115
+ }
116
+ else {
117
+ this.emit("max_reconnect_attempts");
118
+ }
119
+ }
120
+ async stop() {
121
+ return new Promise((resolve) => {
122
+ if (this.ws) {
123
+ this.ws.close();
124
+ this.ws = null;
125
+ }
126
+ resolve();
127
+ });
128
+ }
129
+ }
130
+ exports.DainTunnel = DainTunnel;
131
+ exports.default = {
132
+ createTunnel: (serverUrl) => new DainTunnel(serverUrl),
133
+ };
@@ -0,0 +1,20 @@
1
+ declare class DainTunnelServer {
2
+ private hostname;
3
+ private port;
4
+ private app;
5
+ private server;
6
+ private wss;
7
+ private tunnels;
8
+ private pendingRequests;
9
+ constructor(hostname: string, port: number);
10
+ private setupExpressRoutes;
11
+ private setupWebSocketServer;
12
+ private handleStartMessage;
13
+ private handleResponseMessage;
14
+ private handleHttpRequest;
15
+ private getRequestBody;
16
+ private removeTunnel;
17
+ start(): Promise<void>;
18
+ stop(): Promise<void>;
19
+ }
20
+ export default DainTunnelServer;
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const express_1 = __importDefault(require("express"));
7
+ const http_1 = __importDefault(require("http"));
8
+ const ws_1 = __importDefault(require("ws"));
9
+ const uuid_1 = require("uuid");
10
+ class DainTunnelServer {
11
+ constructor(hostname, port) {
12
+ this.hostname = hostname;
13
+ this.port = port;
14
+ this.tunnels = new Map();
15
+ this.pendingRequests = new Map();
16
+ this.app = (0, express_1.default)();
17
+ this.server = http_1.default.createServer(this.app);
18
+ this.wss = new ws_1.default.Server({ server: this.server });
19
+ this.setupExpressRoutes();
20
+ this.setupWebSocketServer();
21
+ }
22
+ setupExpressRoutes() {
23
+ this.app.use('/:tunnelId', this.handleHttpRequest.bind(this));
24
+ }
25
+ 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);
32
+ }
33
+ else if (data.type === 'response') {
34
+ this.handleResponseMessage(data);
35
+ }
36
+ });
37
+ ws.on('close', () => {
38
+ this.removeTunnel(ws);
39
+ });
40
+ });
41
+ }
42
+ handleStartMessage(ws, data) {
43
+ const tunnelId = (0, uuid_1.v4)();
44
+ this.tunnels.set(tunnelId, { id: tunnelId, ws });
45
+ const tunnelUrl = `${this.hostname}:${this.port}/${tunnelId}`;
46
+ ws.send(JSON.stringify({ type: 'tunnelUrl', url: tunnelUrl }));
47
+ console.log(`New tunnel created: ${tunnelUrl}`);
48
+ }
49
+ handleResponseMessage(data) {
50
+ const pendingRequest = this.pendingRequests.get(data.requestId);
51
+ if (pendingRequest) {
52
+ const { res, startTime } = pendingRequest;
53
+ const endTime = Date.now();
54
+ 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)
59
+ .set(headers)
60
+ .set('Content-Length', bodyBuffer.length.toString())
61
+ .send(bodyBuffer);
62
+ console.log(`Request handled: ${data.requestId}, Duration: ${endTime - startTime}ms`);
63
+ this.pendingRequests.delete(data.requestId);
64
+ }
65
+ }
66
+ async handleHttpRequest(req, res) {
67
+ const tunnelId = req.params.tunnelId;
68
+ const tunnel = this.tunnels.get(tunnelId);
69
+ if (!tunnel) {
70
+ return res.status(404).send('Tunnel not found');
71
+ }
72
+ const requestId = (0, uuid_1.v4)();
73
+ const startTime = Date.now();
74
+ this.pendingRequests.set(requestId, { res, startTime });
75
+ const requestMessage = {
76
+ type: 'request',
77
+ id: requestId,
78
+ method: req.method,
79
+ path: req.url,
80
+ headers: req.headers,
81
+ body: await this.getRequestBody(req)
82
+ };
83
+ tunnel.ws.send(JSON.stringify(requestMessage));
84
+ console.log(`Request forwarded: ${requestId}, Method: ${req.method}, Path: ${req.url}`);
85
+ }
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
+ removeTunnel(ws) {
98
+ for (const [id, tunnel] of this.tunnels.entries()) {
99
+ if (tunnel.ws === ws) {
100
+ this.tunnels.delete(id);
101
+ console.log(`Tunnel removed: ${id}`);
102
+ break;
103
+ }
104
+ }
105
+ }
106
+ async start() {
107
+ return new Promise((resolve) => {
108
+ this.server.listen(this.port, () => {
109
+ console.log(`DainTunnel Server is running on ${this.hostname}:${this.port}`);
110
+ resolve();
111
+ });
112
+ });
113
+ }
114
+ async stop() {
115
+ return new Promise((resolve) => {
116
+ this.wss.close(() => {
117
+ this.server.close(() => {
118
+ resolve();
119
+ });
120
+ });
121
+ });
122
+ }
123
+ }
124
+ exports.default = DainTunnelServer;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const index_1 = __importDefault(require("./index"));
7
+ const port = parseInt(process.env.PORT || '3000', 10);
8
+ const hostname = process.env.HOSTNAME || 'http://localhost';
9
+ const server = new index_1.default(hostname, port);
10
+ server.start()
11
+ .then(() => {
12
+ console.log(`DainTunnel Server started on ${hostname}:${port}`);
13
+ })
14
+ .catch((error) => {
15
+ console.error('Failed to start DainTunnel Server:', error);
16
+ process.exit(1);
17
+ });
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@dainprotocol/tunnel",
3
+ "version": "1.0.0",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "scripts": {
7
+ "build": "tsc",
8
+ "build:types": "tsc --emitDeclarationOnly",
9
+ "test": "jest",
10
+ "test:watch": "jest --watch",
11
+ "prepublishOnly": "npm run build && npm run build:types",
12
+ "start-server": "ts-node src/server/start.ts"
13
+ },
14
+ "keywords": [],
15
+ "author": "Ryan",
16
+ "license": "ISC",
17
+ "dependencies": {
18
+ "@types/express": "^4.17.21",
19
+ "@types/jest": "^29.5.12",
20
+ "@types/node": "^22.5.4",
21
+ "@types/uuid": "^10.0.0",
22
+ "@types/ws": "^8.5.12",
23
+ "express": "^4.19.2",
24
+ "fetch-mock": "^11.1.3",
25
+ "jest": "^29.7.0",
26
+ "ts-jest": "^29.2.5",
27
+ "typescript": "^5.5.4",
28
+ "uuid": "^10.0.0",
29
+ "ws": "^8.18.0"
30
+ }, "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "engines": {
35
+ "node": "20.7.0"
36
+ }
37
+ }