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.
- package/README.md +13 -4
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +13 -13
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/list.js +3 -3
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/port.d.ts +2 -0
- package/dist/commands/port.d.ts.map +1 -0
- package/dist/commands/port.js +192 -0
- package/dist/commands/port.js.map +1 -0
- package/dist/commands/start.d.ts +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +132 -67
- package/dist/commands/start.js.map +1 -1
- package/dist/index.js +56 -44
- package/dist/index.js.map +1 -1
- package/dist/utils/config.d.ts +1 -0
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +9 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/logger.d.ts +18 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +56 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/tunnelClient.d.ts +34 -3
- package/dist/utils/tunnelClient.d.ts.map +1 -1
- package/dist/utils/tunnelClient.js +355 -104
- package/dist/utils/tunnelClient.js.map +1 -1
- package/package.json +3 -3
|
@@ -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
|
|
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
|
|
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;
|
|
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 =
|
|
17
|
-
this.
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
|
135
|
+
case "registered": {
|
|
136
|
+
const wasReconnect = this.tunnelInfo !== null;
|
|
79
137
|
this.tunnelInfo = message.tunnel;
|
|
80
|
-
this.emit(
|
|
81
|
-
|
|
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
|
-
|
|
147
|
+
}
|
|
148
|
+
case "request": {
|
|
84
149
|
await this.handleIncomingRequest(message);
|
|
85
150
|
break;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
151
|
+
}
|
|
152
|
+
case "ws-open": {
|
|
153
|
+
this.handleWebSocketOpen(message);
|
|
89
154
|
break;
|
|
90
|
-
|
|
91
|
-
|
|
155
|
+
}
|
|
156
|
+
case "ws-message": {
|
|
157
|
+
this.handleWebSocketMessage(message);
|
|
92
158
|
break;
|
|
93
|
-
|
|
94
|
-
|
|
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,
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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(
|
|
119
|
-
|
|
335
|
+
this.emit("response", {
|
|
336
|
+
method,
|
|
337
|
+
path,
|
|
338
|
+
status: response.status,
|
|
339
|
+
requestId,
|
|
340
|
+
});
|
|
120
341
|
this.send({
|
|
121
|
-
type:
|
|
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(
|
|
130
|
-
// Send error response
|
|
350
|
+
this.emit("error", error);
|
|
131
351
|
this.send({
|
|
132
|
-
type:
|
|
352
|
+
type: "response",
|
|
133
353
|
requestId,
|
|
134
354
|
status: 502,
|
|
135
|
-
headers: {
|
|
136
|
-
|
|
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 ||
|
|
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(
|
|
154
|
-
|
|
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)
|
|
386
|
+
responseHeaders[key] = Array.isArray(value)
|
|
387
|
+
? value.join(", ")
|
|
388
|
+
: value;
|
|
161
389
|
}
|
|
162
390
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
!contentType.includes(
|
|
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(
|
|
172
|
-
responseHeaders[
|
|
401
|
+
bodyString = bodyBuffer.toString("base64");
|
|
402
|
+
responseHeaders["x-body-encoding"] = "base64";
|
|
173
403
|
}
|
|
174
404
|
else {
|
|
175
|
-
bodyString = bodyBuffer.toString(
|
|
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(
|
|
185
|
-
req.on(
|
|
414
|
+
req.on("error", reject);
|
|
415
|
+
req.on("timeout", () => {
|
|
416
|
+
req.destroy();
|
|
417
|
+
reject(new Error("Request timeout"));
|
|
418
|
+
});
|
|
186
419
|
if (body) {
|
|
187
|
-
|
|
188
|
-
const bodyBuffer =
|
|
189
|
-
? Buffer.from(body,
|
|
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({
|
|
439
|
+
this.send({
|
|
440
|
+
type: "stop",
|
|
441
|
+
tunnelId: this.tunnelInfo.id,
|
|
442
|
+
});
|
|
204
443
|
}
|
|
205
|
-
|
|
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;
|