devportal-tunnel 1.0.1 → 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.
@@ -0,0 +1,56 @@
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.formatLog = formatLog;
7
+ exports.logRequest = logRequest;
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ const fs_1 = __importDefault(require("fs"));
10
+ function formatLog(entry) {
11
+ const methodColor = entry.method === "GET"
12
+ ? chalk_1.default.green
13
+ : entry.method === "POST"
14
+ ? chalk_1.default.blue
15
+ : chalk_1.default.yellow;
16
+ const statusColor = entry.status >= 500
17
+ ? chalk_1.default.red
18
+ : entry.status >= 400
19
+ ? chalk_1.default.yellow
20
+ : chalk_1.default.green;
21
+ return [
22
+ chalk_1.default.gray(`[${entry.time}]`),
23
+ methodColor(entry.method.padEnd(6)),
24
+ chalk_1.default.white(entry.path.padEnd(20)),
25
+ statusColor(entry.status.toString().padEnd(4)),
26
+ chalk_1.default.gray(`${entry.duration}ms`),
27
+ ].join(" ");
28
+ }
29
+ /**
30
+ * Log a request to the terminal and optionally to a file.
31
+ * @param method HTTP method
32
+ * @param path Request path
33
+ * @param status HTTP status code
34
+ * @param duration Duration in ms
35
+ * @param logFile Optional log file path
36
+ */
37
+ function logRequest(method, path, status, duration, logFile) {
38
+ const entry = {
39
+ time: new Date().toLocaleTimeString(),
40
+ method,
41
+ path,
42
+ status,
43
+ duration,
44
+ };
45
+ const line = formatLog(entry);
46
+ console.log(line);
47
+ if (logFile) {
48
+ try {
49
+ fs_1.default.appendFileSync(logFile, line + "\n");
50
+ }
51
+ catch (err) {
52
+ // Ignore file errors for now
53
+ }
54
+ }
55
+ }
56
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":";;;;;AAWA,8BAqBC;AAUD,gCAuBC;AAjED,kDAA0B;AAC1B,4CAAoB;AAUpB,SAAgB,SAAS,CAAC,KAAe;IACvC,MAAM,WAAW,GACf,KAAK,CAAC,MAAM,KAAK,KAAK;QACpB,CAAC,CAAC,eAAK,CAAC,KAAK;QACb,CAAC,CAAC,KAAK,CAAC,MAAM,KAAK,MAAM;YACvB,CAAC,CAAC,eAAK,CAAC,IAAI;YACZ,CAAC,CAAC,eAAK,CAAC,MAAM,CAAC;IACrB,MAAM,WAAW,GACf,KAAK,CAAC,MAAM,IAAI,GAAG;QACjB,CAAC,CAAC,eAAK,CAAC,GAAG;QACX,CAAC,CAAC,KAAK,CAAC,MAAM,IAAI,GAAG;YACnB,CAAC,CAAC,eAAK,CAAC,MAAM;YACd,CAAC,CAAC,eAAK,CAAC,KAAK,CAAC;IAEpB,OAAO;QACL,eAAK,CAAC,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,GAAG,CAAC;QAC7B,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QACnC,eAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAClC,WAAW,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC9C,eAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,QAAQ,IAAI,CAAC;KAClC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACd,CAAC;AAED;;;;;;;GAOG;AACH,SAAgB,UAAU,CACxB,MAAc,EACd,IAAY,EACZ,MAAc,EACd,QAAgB,EAChB,OAAgB;IAEhB,MAAM,KAAK,GAAa;QACtB,IAAI,EAAE,IAAI,IAAI,EAAE,CAAC,kBAAkB,EAAE;QACrC,MAAM;QACN,IAAI;QACJ,MAAM;QACN,QAAQ;KACT,CAAC;IACF,MAAM,IAAI,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9B,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAClB,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,YAAE,CAAC,cAAc,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC,CAAC;QAC1C,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,6BAA6B;QAC/B,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -1,9 +1,10 @@
1
- import { EventEmitter } from 'events';
1
+ import { EventEmitter } from "events";
2
2
  export interface TunnelConfig {
3
3
  serverUrl: string;
4
4
  wsUrl: string;
5
5
  deviceId: string;
6
6
  localPort: number;
7
+ remotePort?: number;
7
8
  subdomain?: string;
8
9
  password?: string;
9
10
  demo?: boolean;
@@ -17,19 +18,49 @@ export interface TunnelInfo {
17
18
  }
18
19
  export declare class TunnelClient extends EventEmitter {
19
20
  private ws;
21
+ /**
22
+ * Local application WebSocket connections.
23
+ *
24
+ * wsId = browser WebSocket ID generated by the server.
25
+ */
26
+ private localWebSockets;
20
27
  private config;
21
28
  private reconnectAttempts;
22
29
  private maxReconnectAttempts;
23
- private reconnectDelay;
30
+ private baseReconnectDelay;
24
31
  private isConnected;
25
32
  private tunnelInfo;
33
+ private pingInterval;
34
+ private isReconnecting;
35
+ private shouldReconnect;
26
36
  constructor(config: TunnelConfig);
27
37
  connect(): Promise<TunnelInfo>;
38
+ private setupWebSocket;
39
+ private scheduleReconnect;
40
+ private startPingInterval;
41
+ private stopPingInterval;
28
42
  private handleMessage;
43
+ /**
44
+ * Browser -> Server -> CLI
45
+ *
46
+ * Server tells CLI to create a local WebSocket.
47
+ */
48
+ private handleWebSocketOpen;
49
+ /**
50
+ * Browser -> Server -> CLI -> Local app
51
+ */
52
+ private handleWebSocketMessage;
53
+ /**
54
+ * Browser -> Server -> CLI
55
+ *
56
+ * Close local WebSocket.
57
+ */
58
+ private handleWebSocketClose;
59
+ private closeAllLocalWebSockets;
29
60
  private handleIncomingRequest;
30
61
  private makeLocalRequest;
31
62
  private send;
32
- stop(): void;
63
+ stop(): Promise<void>;
33
64
  getTunnelInfo(): TunnelInfo | null;
34
65
  }
35
66
  //# sourceMappingURL=tunnelClient.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tunnelClient.d.ts","sourceRoot":"","sources":["../../src/utils/tunnelClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,EAAE,CAA0B;IACpC,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,oBAAoB,CAAK;IACjC,OAAO,CAAC,cAAc,CAAQ;IAC9B,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,UAAU,CAA2B;gBAEjC,MAAM,EAAE,YAAY;IAK1B,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC;YA8DtB,aAAa;YA+Bb,qBAAqB;IAgDnC,OAAO,CAAC,gBAAgB;IAwExB,OAAO,CAAC,IAAI;IAMZ,IAAI,IAAI,IAAI;IAOZ,aAAa,IAAI,UAAU,GAAG,IAAI;CAGnC"}
1
+ {"version":3,"file":"tunnelClient.d.ts","sourceRoot":"","sources":["../../src/utils/tunnelClient.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,qBAAa,YAAa,SAAQ,YAAY;IAC5C,OAAO,CAAC,EAAE,CAA0B;IAEpC;;;;OAIG;IACH,OAAO,CAAC,eAAe,CAAgC;IAEvD,OAAO,CAAC,MAAM,CAAe;IAE7B,OAAO,CAAC,iBAAiB,CAAK;IAE9B,OAAO,CAAC,oBAAoB,CAAM;IAElC,OAAO,CAAC,kBAAkB,CAAQ;IAElC,OAAO,CAAC,WAAW,CAAS;IAE5B,OAAO,CAAC,UAAU,CAA2B;IAE7C,OAAO,CAAC,YAAY,CAA+B;IAEnD,OAAO,CAAC,cAAc,CAAS;IAE/B,OAAO,CAAC,eAAe,CAAQ;gBAEnB,MAAM,EAAE,YAAY;IAM1B,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC;IAMpC,OAAO,CAAC,cAAc;IA4FtB,OAAO,CAAC,iBAAiB;IA8BzB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,gBAAgB;YAQV,aAAa;IA8E3B;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAwF3B;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAsB9B;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAmB5B,OAAO,CAAC,uBAAuB;YAajB,qBAAqB;IA+EnC,OAAO,CAAC,gBAAgB;IAiGxB,OAAO,CAAC,IAAI;IAMN,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA+B3B,aAAa,IAAI,UAAU,GAAG,IAAI;CAGnC"}
@@ -12,113 +12,334 @@ class TunnelClient extends events_1.EventEmitter {
12
12
  constructor(config) {
13
13
  super();
14
14
  this.ws = null;
15
+ /**
16
+ * Local application WebSocket connections.
17
+ *
18
+ * wsId = browser WebSocket ID generated by the server.
19
+ */
20
+ this.localWebSockets = new Map();
15
21
  this.reconnectAttempts = 0;
16
- this.maxReconnectAttempts = 5;
17
- this.reconnectDelay = 1000;
22
+ this.maxReconnectAttempts = 10;
23
+ this.baseReconnectDelay = 1000;
18
24
  this.isConnected = false;
19
25
  this.tunnelInfo = null;
26
+ this.pingInterval = null;
27
+ this.isReconnecting = false;
28
+ this.shouldReconnect = true;
20
29
  this.config = config;
21
30
  }
22
31
  async connect() {
23
32
  return new Promise((resolve, reject) => {
24
- try {
25
- this.ws = new ws_1.default(this.config.wsUrl);
26
- this.ws.on('open', () => {
27
- this.isConnected = true;
28
- this.reconnectAttempts = 0;
29
- // Register tunnel
30
- this.send({
31
- type: 'register',
32
- deviceId: this.config.deviceId,
33
- localPort: this.config.localPort,
34
- subdomain: this.config.subdomain,
35
- password: this.config.password,
36
- demo: this.config.demo,
37
- });
38
- });
39
- this.ws.on('message', async (data) => {
40
- try {
41
- const message = JSON.parse(data.toString());
42
- await this.handleMessage(message, resolve, reject);
43
- }
44
- catch (error) {
45
- console.error('Failed to parse message:', error);
46
- }
47
- });
48
- this.ws.on('close', () => {
49
- this.isConnected = false;
50
- this.emit('disconnected');
51
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
52
- this.reconnectAttempts++;
53
- console.log(chalk_1.default.yellow(`Reconnecting... (attempt ${this.reconnectAttempts})`));
54
- setTimeout(() => this.connect(), this.reconnectDelay * this.reconnectAttempts);
55
- }
56
- });
57
- this.ws.on('error', (error) => {
58
- if (!this.isConnected) {
59
- reject(error);
60
- }
61
- this.emit('error', error);
62
- });
63
- // Ping to keep connection alive
64
- const pingInterval = setInterval(() => {
65
- if (this.isConnected && this.ws?.readyState === ws_1.default.OPEN) {
66
- this.send({ type: 'ping' });
67
- }
68
- }, 30000);
69
- this.on('disconnected', () => clearInterval(pingInterval));
33
+ this.setupWebSocket(resolve, reject);
34
+ });
35
+ }
36
+ setupWebSocket(resolve, reject) {
37
+ try {
38
+ // Clean up existing control connection
39
+ if (this.ws) {
40
+ this.ws.removeAllListeners();
41
+ if (this.ws.readyState === ws_1.default.OPEN ||
42
+ this.ws.readyState === ws_1.default.CONNECTING) {
43
+ this.ws.close();
44
+ }
70
45
  }
71
- catch (error) {
46
+ this.ws = new ws_1.default(this.config.wsUrl);
47
+ const ws = this.ws;
48
+ ws.on("open", () => {
49
+ this.isConnected = true;
50
+ this.isReconnecting = false;
51
+ this.reconnectAttempts = 0;
52
+ this.send({
53
+ type: "register",
54
+ deviceId: this.config.deviceId,
55
+ localPort: this.config.localPort,
56
+ remotePort: this.config.remotePort,
57
+ subdomain: this.tunnelInfo?.name || this.config.subdomain,
58
+ password: this.config.password,
59
+ demo: this.config.demo,
60
+ });
61
+ this.startPingInterval();
62
+ });
63
+ ws.on("message", async (data) => {
64
+ try {
65
+ const message = JSON.parse(data.toString());
66
+ await this.handleMessage(message, resolve, reject);
67
+ }
68
+ catch (error) {
69
+ console.error("Failed to parse server message:", error);
70
+ }
71
+ });
72
+ ws.on("close", () => {
73
+ this.isConnected = false;
74
+ this.stopPingInterval();
75
+ this.closeAllLocalWebSockets();
76
+ if (!this.shouldReconnect) {
77
+ this.emit("disconnected");
78
+ return;
79
+ }
80
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
81
+ this.scheduleReconnect();
82
+ }
83
+ else {
84
+ console.log(chalk_1.default.red("\nMax reconnection attempts reached. Tunnel stopped."));
85
+ console.log(chalk_1.default.gray("Run the command again to start a new tunnel."));
86
+ this.emit("disconnected");
87
+ process.exit(1);
88
+ }
89
+ });
90
+ ws.on("error", (error) => {
91
+ if (!this.isConnected && !this.isReconnecting && reject) {
92
+ reject(error);
93
+ }
94
+ this.emit("error", error);
95
+ });
96
+ }
97
+ catch (error) {
98
+ if (reject) {
72
99
  reject(error);
73
100
  }
74
- });
101
+ }
102
+ }
103
+ scheduleReconnect() {
104
+ if (this.isReconnecting) {
105
+ return;
106
+ }
107
+ this.isReconnecting = true;
108
+ this.reconnectAttempts++;
109
+ const delay = Math.min(this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1) +
110
+ Math.random() * 1000, 30000);
111
+ console.log(chalk_1.default.yellow(`\nConnection lost. Reconnecting in ${Math.round(delay / 1000)}s... (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts})`));
112
+ setTimeout(() => {
113
+ this.emit("reconnecting", this.reconnectAttempts);
114
+ this.setupWebSocket();
115
+ }, delay);
116
+ }
117
+ startPingInterval() {
118
+ this.stopPingInterval();
119
+ this.pingInterval = setInterval(() => {
120
+ if (this.isConnected && this.ws?.readyState === ws_1.default.OPEN) {
121
+ this.send({
122
+ type: "ping",
123
+ });
124
+ }
125
+ }, 15000);
126
+ }
127
+ stopPingInterval() {
128
+ if (this.pingInterval) {
129
+ clearInterval(this.pingInterval);
130
+ this.pingInterval = null;
131
+ }
75
132
  }
76
133
  async handleMessage(message, resolve, reject) {
77
134
  switch (message.type) {
78
- case 'registered':
135
+ case "registered": {
136
+ const wasReconnect = this.tunnelInfo !== null;
79
137
  this.tunnelInfo = message.tunnel;
80
- this.emit('registered', message.tunnel);
81
- resolve(message.tunnel);
138
+ this.emit("registered", message.tunnel);
139
+ if (wasReconnect) {
140
+ console.log(chalk_1.default.green(`\nReconnected! Tunnel restored: ${message.tunnel.url}`));
141
+ this.emit("reconnected", message.tunnel);
142
+ }
143
+ else if (resolve) {
144
+ resolve(message.tunnel);
145
+ }
82
146
  break;
83
- case 'request':
147
+ }
148
+ case "request": {
84
149
  await this.handleIncomingRequest(message);
85
150
  break;
86
- case 'error':
87
- console.error(chalk_1.default.red('Server error:'), message.message);
88
- reject(new Error(message.message));
151
+ }
152
+ case "ws-open": {
153
+ this.handleWebSocketOpen(message);
89
154
  break;
90
- case 'pong':
91
- // Connection alive
155
+ }
156
+ case "ws-message": {
157
+ this.handleWebSocketMessage(message);
92
158
  break;
93
- case 'stopped':
94
- this.emit('stopped', message.tunnelId);
159
+ }
160
+ case "ws-close": {
161
+ this.handleWebSocketClose(message);
95
162
  break;
163
+ }
164
+ case "error": {
165
+ console.error(chalk_1.default.red("Server error:"), message.message);
166
+ if (reject) {
167
+ reject(new Error(message.message));
168
+ }
169
+ break;
170
+ }
171
+ case "pong": {
172
+ break;
173
+ }
174
+ case "stopped": {
175
+ this.emit("stopped", message.tunnelId);
176
+ break;
177
+ }
178
+ default: {
179
+ console.warn("Unknown server message:", message.type);
180
+ }
96
181
  }
97
182
  }
183
+ /**
184
+ * Browser -> Server -> CLI
185
+ *
186
+ * Server tells CLI to create a local WebSocket.
187
+ */
188
+ handleWebSocketOpen(message) {
189
+ const { wsId, path, headers } = message;
190
+ if (!wsId) {
191
+ return;
192
+ }
193
+ const safePath = path && path !== "undefined" ? path : "/";
194
+ const localUrl = `ws://localhost:${this.config.localPort}${safePath}`;
195
+ console.log(chalk_1.default.gray(`WebSocket opening: ${localUrl}`));
196
+ const localHeaders = {
197
+ host: `localhost:${this.config.localPort}`,
198
+ };
199
+ // Forward useful headers but remove proxy-specific ones.
200
+ if (headers && typeof headers === "object") {
201
+ for (const [key, value] of Object.entries(headers)) {
202
+ if (typeof value === "string" &&
203
+ ![
204
+ "host",
205
+ "connection",
206
+ "upgrade",
207
+ "sec-websocket-key",
208
+ "sec-websocket-version",
209
+ "sec-websocket-extensions",
210
+ "content-length",
211
+ "x-forwarded-host",
212
+ "x-forwarded-proto",
213
+ "x-forwarded-for",
214
+ ].includes(key.toLowerCase())) {
215
+ localHeaders[key] = value;
216
+ }
217
+ }
218
+ }
219
+ const localWs = new ws_1.default(localUrl, {
220
+ headers: localHeaders,
221
+ });
222
+ this.localWebSockets.set(wsId, localWs);
223
+ localWs.on("open", () => {
224
+ console.log(chalk_1.default.gray(`WebSocket connected locally: ${wsId}`));
225
+ this.send({
226
+ type: "ws-opened",
227
+ wsId,
228
+ });
229
+ });
230
+ localWs.on("message", (data, isBinary) => {
231
+ this.send({
232
+ type: "ws-message",
233
+ wsId,
234
+ data: Buffer.from(data).toString("base64"),
235
+ binary: isBinary,
236
+ });
237
+ });
238
+ localWs.on("close", () => {
239
+ this.localWebSockets.delete(wsId);
240
+ this.send({
241
+ type: "ws-close",
242
+ wsId,
243
+ });
244
+ });
245
+ localWs.on("error", (error) => {
246
+ console.error(chalk_1.default.red(`Local WebSocket error (${wsId}):`), error.message);
247
+ this.localWebSockets.delete(wsId);
248
+ this.send({
249
+ type: "ws-close",
250
+ wsId,
251
+ });
252
+ });
253
+ }
254
+ /**
255
+ * Browser -> Server -> CLI -> Local app
256
+ */
257
+ handleWebSocketMessage(message) {
258
+ const { wsId, data, binary } = message;
259
+ const localWs = this.localWebSockets.get(wsId);
260
+ if (!localWs || localWs.readyState !== ws_1.default.OPEN) {
261
+ return;
262
+ }
263
+ try {
264
+ const payload = Buffer.from(data, "base64");
265
+ if (binary) {
266
+ localWs.send(payload);
267
+ }
268
+ else {
269
+ localWs.send(payload.toString("utf8"));
270
+ }
271
+ }
272
+ catch (error) {
273
+ console.error("Failed to forward WebSocket message:", error);
274
+ }
275
+ }
276
+ /**
277
+ * Browser -> Server -> CLI
278
+ *
279
+ * Close local WebSocket.
280
+ */
281
+ handleWebSocketClose(message) {
282
+ const { wsId } = message;
283
+ const localWs = this.localWebSockets.get(wsId);
284
+ if (!localWs) {
285
+ return;
286
+ }
287
+ this.localWebSockets.delete(wsId);
288
+ if (localWs.readyState === ws_1.default.OPEN ||
289
+ localWs.readyState === ws_1.default.CONNECTING) {
290
+ localWs.close();
291
+ }
292
+ }
293
+ closeAllLocalWebSockets() {
294
+ for (const localWs of this.localWebSockets.values()) {
295
+ if (localWs.readyState === ws_1.default.OPEN ||
296
+ localWs.readyState === ws_1.default.CONNECTING) {
297
+ localWs.close();
298
+ }
299
+ }
300
+ this.localWebSockets.clear();
301
+ }
98
302
  async handleIncomingRequest(message) {
99
- const { requestId, method, path, headers, query, body } = message;
100
- // Log the request
101
- this.emit('request', { method, path, requestId });
102
- // Build URL with query params
103
- let url = `http://localhost:${this.config.localPort}${path}`;
104
- if (query && Object.keys(query).length > 0) {
105
- const params = new URLSearchParams(query);
106
- url += `?${params.toString()}`;
107
- }
108
- // Add auth header if configured
109
- const requestHeaders = { ...headers };
303
+ const { requestId, method, path, headers, body } = message;
304
+ this.emit("request", {
305
+ method,
306
+ path,
307
+ requestId,
308
+ });
309
+ const safePath = path && path !== "undefined" ? path : "/";
310
+ const url = `http://localhost:${this.config.localPort}${safePath}`;
311
+ const requestHeaders = {
312
+ ...headers,
313
+ host: `localhost:${this.config.localPort}`,
314
+ };
315
+ delete requestHeaders["content-length"];
316
+ delete requestHeaders["transfer-encoding"];
317
+ delete requestHeaders["connection"];
318
+ delete requestHeaders["sec-fetch-site"];
319
+ delete requestHeaders["sec-fetch-mode"];
320
+ delete requestHeaders["sec-fetch-dest"];
321
+ delete requestHeaders["sec-fetch-user"];
322
+ delete requestHeaders["origin"];
323
+ delete requestHeaders["referer"];
324
+ delete requestHeaders["x-forwarded-host"];
325
+ delete requestHeaders["x-forwarded-proto"];
326
+ delete requestHeaders["x-forwarded-for"];
110
327
  if (this.config.authHeader) {
111
- const [key, value] = this.config.authHeader.split(': ');
328
+ const [key, value] = this.config.authHeader.split(": ");
112
329
  if (key && value) {
113
330
  requestHeaders[key] = value;
114
331
  }
115
332
  }
116
333
  try {
117
334
  const response = await this.makeLocalRequest(method, url, requestHeaders, body);
118
- this.emit('response', { method, path, status: response.status, requestId });
119
- // Send response back to server
335
+ this.emit("response", {
336
+ method,
337
+ path,
338
+ status: response.status,
339
+ requestId,
340
+ });
120
341
  this.send({
121
- type: 'response',
342
+ type: "response",
122
343
  requestId,
123
344
  status: response.status,
124
345
  headers: response.headers,
@@ -126,14 +347,18 @@ class TunnelClient extends events_1.EventEmitter {
126
347
  });
127
348
  }
128
349
  catch (error) {
129
- this.emit('error', error);
130
- // Send error response
350
+ this.emit("error", error);
131
351
  this.send({
132
- type: 'response',
352
+ type: "response",
133
353
  requestId,
134
354
  status: 502,
135
- headers: { 'content-type': 'application/json' },
136
- body: JSON.stringify({ error: 'Failed to connect to local server', message: error.message }),
355
+ headers: {
356
+ "content-type": "application/json",
357
+ },
358
+ body: JSON.stringify({
359
+ error: "Failed to connect to local server",
360
+ message: error.message,
361
+ }),
137
362
  });
138
363
  }
139
364
  }
@@ -142,7 +367,7 @@ class TunnelClient extends events_1.EventEmitter {
142
367
  const urlObj = new URL(url);
143
368
  const options = {
144
369
  hostname: urlObj.hostname,
145
- port: urlObj.port || 80,
370
+ port: Number(urlObj.port) || this.config.localPort,
146
371
  path: urlObj.pathname + urlObj.search,
147
372
  method,
148
373
  headers,
@@ -150,29 +375,34 @@ class TunnelClient extends events_1.EventEmitter {
150
375
  };
151
376
  const req = http_1.default.request(options, (res) => {
152
377
  const chunks = [];
153
- res.on('data', (chunk) => chunks.push(chunk));
154
- res.on('end', () => {
378
+ res.on("data", (chunk) => {
379
+ chunks.push(chunk);
380
+ });
381
+ res.on("end", () => {
155
382
  const bodyBuffer = Buffer.concat(chunks);
156
383
  const responseHeaders = {};
157
- // Convert headers
158
384
  for (const [key, value] of Object.entries(res.headers)) {
159
385
  if (value) {
160
- responseHeaders[key] = Array.isArray(value) ? value.join(', ') : value;
386
+ responseHeaders[key] = Array.isArray(value)
387
+ ? value.join(", ")
388
+ : value;
161
389
  }
162
390
  }
163
- // Check if body is binary
164
- const contentType = responseHeaders['content-type'] || '';
165
- const isBinary = !contentType.includes('text') &&
166
- !contentType.includes('json') &&
167
- !contentType.includes('xml') &&
168
- !contentType.includes('javascript');
391
+ delete responseHeaders["content-length"];
392
+ delete responseHeaders["transfer-encoding"];
393
+ delete responseHeaders["connection"];
394
+ const contentType = responseHeaders["content-type"] || "";
395
+ const isBinary = !contentType.includes("text") &&
396
+ !contentType.includes("json") &&
397
+ !contentType.includes("xml") &&
398
+ !contentType.includes("javascript");
169
399
  let bodyString;
170
400
  if (isBinary) {
171
- bodyString = bodyBuffer.toString('base64');
172
- responseHeaders['x-body-encoding'] = 'base64';
401
+ bodyString = bodyBuffer.toString("base64");
402
+ responseHeaders["x-body-encoding"] = "base64";
173
403
  }
174
404
  else {
175
- bodyString = bodyBuffer.toString('utf-8');
405
+ bodyString = bodyBuffer.toString("utf-8");
176
406
  }
177
407
  resolve({
178
408
  status: res.statusCode || 500,
@@ -181,13 +411,16 @@ class TunnelClient extends events_1.EventEmitter {
181
411
  });
182
412
  });
183
413
  });
184
- req.on('error', reject);
185
- req.on('timeout', () => reject(new Error('Request timeout')));
414
+ req.on("error", reject);
415
+ req.on("timeout", () => {
416
+ req.destroy();
417
+ reject(new Error("Request timeout"));
418
+ });
186
419
  if (body) {
187
- // Decode base64 body if it was encoded
188
- const bodyBuffer = body.startsWith('ey') || body.includes('=')
189
- ? Buffer.from(body, 'base64')
190
- : Buffer.from(body);
420
+ const bodyEncoding = headers?.["x-body-encoding"];
421
+ const bodyBuffer = bodyEncoding === "base64"
422
+ ? Buffer.from(body, "base64")
423
+ : Buffer.from(body, "utf-8");
191
424
  req.write(bodyBuffer);
192
425
  }
193
426
  req.end();
@@ -198,11 +431,29 @@ class TunnelClient extends events_1.EventEmitter {
198
431
  this.ws.send(JSON.stringify(data));
199
432
  }
200
433
  }
201
- stop() {
434
+ async stop() {
435
+ this.shouldReconnect = false;
436
+ this.stopPingInterval();
437
+ this.closeAllLocalWebSockets();
202
438
  if (this.tunnelInfo) {
203
- this.send({ type: 'stop', tunnelId: this.tunnelInfo.id });
439
+ this.send({
440
+ type: "stop",
441
+ tunnelId: this.tunnelInfo.id,
442
+ });
204
443
  }
205
- this.ws?.close();
444
+ return new Promise((resolve) => {
445
+ if (this.ws?.readyState === ws_1.default.OPEN) {
446
+ const timeout = setTimeout(() => resolve(), 500);
447
+ this.ws.once("close", () => {
448
+ clearTimeout(timeout);
449
+ resolve();
450
+ });
451
+ this.ws.close();
452
+ }
453
+ else {
454
+ resolve();
455
+ }
456
+ });
206
457
  }
207
458
  getTunnelInfo() {
208
459
  return this.tunnelInfo;