pinggy 0.1.1 → 0.1.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.
- package/dist/cli/buildConfig.js +15 -2
- package/dist/cli/defaults.js +1 -1
- package/dist/cli/help.js +10 -10
- package/dist/cli/options.js +1 -1
- package/dist/cli/starCli.js +62 -6
- package/dist/index.js +2 -2
- package/dist/tui/index.js +10 -4
- package/dist/tunnel_manager/TunnelManager.js +187 -1
- package/dist/types.js +1 -0
- package/dist/utils/FileServer.js +112 -0
- package/dist/utils/htmlTemplates.js +135 -0
- package/dist/utils/parseArgs.js +4 -1
- package/dist/workers/file_serve_worker.js +41 -0
- package/dist/{cli → workers}/worker.js +12 -2
- package/package.json +3 -2
- package/src/cli/buildConfig.ts +16 -2
- package/src/cli/defaults.ts +1 -1
- package/src/cli/help.ts +12 -10
- package/src/cli/options.ts +1 -1
- package/src/cli/starCli.tsx +101 -10
- package/src/index.ts +2 -2
- package/src/tui/index.tsx +16 -4
- package/src/tunnel_manager/TunnelManager.ts +211 -5
- package/src/types.ts +2 -1
- package/src/utils/FileServer.ts +112 -0
- package/src/utils/htmlTemplates.ts +146 -0
- package/src/utils/parseArgs.ts +11 -1
- package/src/workers/file_serve_worker.ts +33 -0
- package/src/{cli → workers}/worker.ts +24 -9
package/dist/cli/buildConfig.js
CHANGED
|
@@ -181,9 +181,12 @@ function parseForwarding(forwarding) {
|
|
|
181
181
|
}
|
|
182
182
|
function parseReverseTunnelAddr(finalConfig, values) {
|
|
183
183
|
const reverseTunnel = values.R;
|
|
184
|
-
if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
|
|
184
|
+
if ((!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) && !values.localport) {
|
|
185
185
|
return new Error("local port not specified. Please use '-h' option for help.");
|
|
186
186
|
}
|
|
187
|
+
if (!Array.isArray(reverseTunnel) || reverseTunnel.length === 0) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
187
190
|
const forwarding = parseForwarding(reverseTunnel[0]);
|
|
188
191
|
if (forwarding instanceof Error) {
|
|
189
192
|
return forwarding;
|
|
@@ -277,6 +280,13 @@ function isSaveConfOption(values) {
|
|
|
277
280
|
}
|
|
278
281
|
return null;
|
|
279
282
|
}
|
|
283
|
+
function parseServe(finalConfig, values) {
|
|
284
|
+
const sv = values.serve;
|
|
285
|
+
if (typeof sv !== 'string' || sv.trim().length === 0)
|
|
286
|
+
return null;
|
|
287
|
+
finalConfig.serve = sv;
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
280
290
|
export function buildFinalConfig(values, positionals) {
|
|
281
291
|
let token;
|
|
282
292
|
let server;
|
|
@@ -297,7 +307,7 @@ export function buildFinalConfig(values, positionals) {
|
|
|
297
307
|
qrCode = userParse.qrCode;
|
|
298
308
|
const remainingPositionals = userParse.remaining;
|
|
299
309
|
const initialTunnel = (type || values.type);
|
|
300
|
-
finalConfig = Object.assign(Object.assign({}, defaultOptions), { configid: uuidv4(), token: token || (typeof values.token === 'string' ? values.token : ''), serverAddress: server || defaultOptions.serverAddress, tunnelType: initialTunnel ? [initialTunnel] : defaultOptions.tunnelType, NoTUI: values.
|
|
310
|
+
finalConfig = Object.assign(Object.assign({}, defaultOptions), { configid: uuidv4(), token: token || (typeof values.token === 'string' ? values.token : ''), serverAddress: server || defaultOptions.serverAddress, tunnelType: initialTunnel ? [initialTunnel] : defaultOptions.tunnelType, NoTUI: values.notui || false, qrCode: qrCode || false });
|
|
301
311
|
parseType(finalConfig, values, type);
|
|
302
312
|
// Apply token
|
|
303
313
|
parseToken(finalConfig, token || values.token);
|
|
@@ -313,6 +323,9 @@ export function buildFinalConfig(values, positionals) {
|
|
|
313
323
|
const lErr = parseLocalTunnelAddr(finalConfig, values);
|
|
314
324
|
if (lErr instanceof Error)
|
|
315
325
|
throw lErr;
|
|
326
|
+
const serveErr = parseServe(finalConfig, values);
|
|
327
|
+
if (serveErr instanceof Error)
|
|
328
|
+
throw serveErr;
|
|
316
329
|
// Apply force flag if indicated via user
|
|
317
330
|
if (forceFlag)
|
|
318
331
|
finalConfig.force = true;
|
package/dist/cli/defaults.js
CHANGED
package/dist/cli/help.js
CHANGED
|
@@ -2,7 +2,7 @@ import { cliOptions } from "./options.js";
|
|
|
2
2
|
export function printHelpMessage() {
|
|
3
3
|
console.log("\nPinggy CLI Tool - Create secure tunnels to your localhost.");
|
|
4
4
|
console.log("\nUsage:");
|
|
5
|
-
console.log("
|
|
5
|
+
console.log(" pinggy [options] -l <port>\n");
|
|
6
6
|
console.log("Options:");
|
|
7
7
|
for (const [key, value] of Object.entries(cliOptions)) {
|
|
8
8
|
if (value.hidden)
|
|
@@ -21,17 +21,17 @@ export function printHelpMessage() {
|
|
|
21
21
|
console.log(" r:Key Remove header");
|
|
22
22
|
console.log(" b:user:pass Basic auth");
|
|
23
23
|
console.log(" k:BEARER Bearer token");
|
|
24
|
-
console.log(" w:192.168.1.0/24 IP whitelist (CIDR)
|
|
25
|
-
console.log("
|
|
24
|
+
console.log(" w:192.168.1.0/24 IP whitelist (CIDR)");
|
|
25
|
+
console.log("\nExamples (User-friendly):");
|
|
26
|
+
console.log(" pinggy -l 3000 # HTTP(S) tunnel to localhost port 3000");
|
|
27
|
+
console.log(" pinggy --type tcp -l 22 # TCP tunnel for SSH (port 22)");
|
|
28
|
+
console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel to port 8080 with debugger running at localhost:4300");
|
|
29
|
+
console.log(" pinggy --token mytoken -l 3000 # Authenticated tunnel");
|
|
30
|
+
console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
|
|
31
|
+
console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
|
|
32
|
+
console.log("\nExamples (SSH-style):");
|
|
26
33
|
console.log(" pinggy -R0:localhost:3000 # Basic HTTP tunnel");
|
|
27
34
|
console.log(" pinggy --type tcp -R0:localhost:22 # TCP tunnel for SSH");
|
|
28
35
|
console.log(" pinggy -R0:localhost:8080 -L4300:localhost:4300 # HTTP tunnel with debugger");
|
|
29
36
|
console.log(" pinggy tcp@ap.example.com -R0:localhost:22 # TCP tunnel to region\n");
|
|
30
|
-
console.log("Examples (User-friendly):");
|
|
31
|
-
console.log(" pinggy -p 3000 # Basic HTTP tunnel");
|
|
32
|
-
console.log(" pinggy --type tcp -p 22 # TCP tunnel for SSH");
|
|
33
|
-
console.log(" pinggy -l 8080 -d 4300 # HTTP tunnel with debugger");
|
|
34
|
-
console.log(" pinggy mytoken@a.example.com -p 3000 # Authenticated tunnel");
|
|
35
|
-
console.log(" pinggy x:https x:xff -l https://localhost:8443 # HTTPS-only + XFF");
|
|
36
|
-
console.log(" pinggy w:192.168.1.0/24 -l 8080 # IP whitelist restriction");
|
|
37
37
|
}
|
package/dist/cli/options.js
CHANGED
|
@@ -28,7 +28,7 @@ export const cliOptions = {
|
|
|
28
28
|
// Remote Control
|
|
29
29
|
'remote-management': { type: 'string', description: 'Enable remote management of tunnels with token. Eg. --remote-management API_KEY' },
|
|
30
30
|
manage: { type: 'string', description: 'Provide a server address to manage tunnels. Eg --manage dashboard.pinggy.io' },
|
|
31
|
-
|
|
31
|
+
notui: { type: 'boolean', description: 'Disable TUI in remote management mode' },
|
|
32
32
|
// Misc
|
|
33
33
|
version: { type: 'boolean', description: 'Print version' },
|
|
34
34
|
// Help
|
package/dist/cli/starCli.js
CHANGED
|
@@ -15,11 +15,29 @@ import { withFullScreen } from "fullscreen-ink";
|
|
|
15
15
|
import path from "node:path";
|
|
16
16
|
import { Worker } from "node:worker_threads";
|
|
17
17
|
import { getFreePort } from "../utils/getFreePort.js";
|
|
18
|
+
import { fileURLToPath } from "url";
|
|
19
|
+
import { logger } from "../logger.js";
|
|
20
|
+
import React, { useState } from "react";
|
|
18
21
|
const TunnelData = {
|
|
19
22
|
urls: null,
|
|
20
23
|
greet: null,
|
|
21
24
|
usage: null,
|
|
22
25
|
};
|
|
26
|
+
let activeTui = null;
|
|
27
|
+
let disconnectState = null;
|
|
28
|
+
let updateDisconnectState = null;
|
|
29
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
30
|
+
const __dirname = path.dirname(__filename);
|
|
31
|
+
const TunnelTuiWrapper = ({ finalConfig, urls, greet }) => {
|
|
32
|
+
const [disconnectInfo, setDisconnectInfo] = useState(null);
|
|
33
|
+
React.useEffect(() => {
|
|
34
|
+
updateDisconnectState = setDisconnectInfo;
|
|
35
|
+
return () => {
|
|
36
|
+
updateDisconnectState = null;
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
return (_jsx(TunnelTui, { urls: urls !== null && urls !== void 0 ? urls : [], greet: greet !== null && greet !== void 0 ? greet : "", tunnelConfig: finalConfig, disconnectInfo: disconnectInfo }));
|
|
40
|
+
};
|
|
23
41
|
export function startCli(finalConfig, manager) {
|
|
24
42
|
return __awaiter(this, void 0, void 0, function* () {
|
|
25
43
|
if (!finalConfig.NoTUI && finalConfig.webDebugger === "") {
|
|
@@ -27,13 +45,13 @@ export function startCli(finalConfig, manager) {
|
|
|
27
45
|
const freePort = yield getFreePort(finalConfig.webDebugger || "");
|
|
28
46
|
finalConfig.webDebugger = `localhost:${freePort}`;
|
|
29
47
|
}
|
|
30
|
-
const workerPath = path.resolve("
|
|
48
|
+
const workerPath = path.resolve(__dirname, "../workers/worker.js");
|
|
31
49
|
try {
|
|
32
50
|
const worker = new Worker(workerPath, {
|
|
33
51
|
workerData: { finalConfig },
|
|
34
52
|
});
|
|
35
53
|
worker.on("message", (msg) => __awaiter(this, void 0, void 0, function* () {
|
|
36
|
-
var _a, _b, _c, _d, _e
|
|
54
|
+
var _a, _b, _c, _d, _e;
|
|
37
55
|
switch (msg.type) {
|
|
38
56
|
case "created":
|
|
39
57
|
CLIPrinter.startSpinner(msg.message);
|
|
@@ -70,15 +88,52 @@ export function startCli(finalConfig, manager) {
|
|
|
70
88
|
CLIPrinter.info(msg.message || "Status update from worker");
|
|
71
89
|
break;
|
|
72
90
|
case "usage":
|
|
73
|
-
console.log("Usage update:", msg.usage);
|
|
74
91
|
TunnelData.usage = msg.usage;
|
|
75
92
|
(_d = globalThis.__PINGGY_TUNNEL_STATS__) === null || _d === void 0 ? void 0 : _d.call(globalThis, msg.usage);
|
|
76
93
|
break;
|
|
77
94
|
case "TUI":
|
|
78
95
|
if (!finalConfig.NoTUI) {
|
|
79
|
-
const tui = withFullScreen(_jsx(
|
|
80
|
-
|
|
96
|
+
const tui = withFullScreen(_jsx(TunnelTuiWrapper, { finalConfig: finalConfig, urls: TunnelData.urls, greet: TunnelData.greet }));
|
|
97
|
+
activeTui = tui;
|
|
98
|
+
try {
|
|
99
|
+
yield tui.start();
|
|
100
|
+
yield tui.waitUntilExit();
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
logger.warn("TUI error", e);
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
activeTui = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case "warnings":
|
|
111
|
+
CLIPrinter.warn(msg.message);
|
|
112
|
+
break;
|
|
113
|
+
case "disconnected":
|
|
114
|
+
if (activeTui && updateDisconnectState) {
|
|
115
|
+
disconnectState = {
|
|
116
|
+
disconnected: true,
|
|
117
|
+
error: msg.error,
|
|
118
|
+
messages: msg.messages
|
|
119
|
+
};
|
|
120
|
+
updateDisconnectState(disconnectState);
|
|
121
|
+
try {
|
|
122
|
+
// Wait for Ink to fully exit
|
|
123
|
+
yield activeTui.waitUntilExit();
|
|
124
|
+
}
|
|
125
|
+
catch (e) {
|
|
126
|
+
logger.warn("Failed to wait for TUI exit", e);
|
|
127
|
+
}
|
|
128
|
+
finally {
|
|
129
|
+
activeTui = null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if ((_e = msg.messages) === null || _e === void 0 ? void 0 : _e.length) {
|
|
133
|
+
msg.messages.forEach((m) => CLIPrinter.print(m));
|
|
81
134
|
}
|
|
135
|
+
// Exit ONLY after fullscreen ink has restored the terminal
|
|
136
|
+
process.exit(0);
|
|
82
137
|
break;
|
|
83
138
|
case "error":
|
|
84
139
|
CLIPrinter.error(msg.message || "Unknown error from worker");
|
|
@@ -86,7 +141,8 @@ export function startCli(finalConfig, manager) {
|
|
|
86
141
|
}
|
|
87
142
|
}));
|
|
88
143
|
worker.on("error", (err) => {
|
|
89
|
-
|
|
144
|
+
logger.error("Worker thread error:", err);
|
|
145
|
+
CLIPrinter.error(`${err}`);
|
|
90
146
|
});
|
|
91
147
|
worker.on("exit", (code) => {
|
|
92
148
|
if (code !== 0) {
|
package/dist/index.js
CHANGED
|
@@ -22,10 +22,10 @@ function main() {
|
|
|
22
22
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
23
|
try {
|
|
24
24
|
// Parse arguments from the command line
|
|
25
|
-
const { values, positionals } = parseCliArgs(cliOptions);
|
|
25
|
+
const { values, positionals, hasAnyArgs } = parseCliArgs(cliOptions);
|
|
26
26
|
// Configure logger from CLI args
|
|
27
27
|
configureLogger(values);
|
|
28
|
-
if (values.help) {
|
|
28
|
+
if (!hasAnyArgs || values.help) {
|
|
29
29
|
printHelpMessage();
|
|
30
30
|
return;
|
|
31
31
|
}
|
package/dist/tui/index.js
CHANGED
|
@@ -8,8 +8,8 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
});
|
|
9
9
|
};
|
|
10
10
|
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
11
|
-
import { useState } from "react";
|
|
12
|
-
import { Box, Text, useInput } from "ink";
|
|
11
|
+
import { useEffect, useState } from "react";
|
|
12
|
+
import { Box, Text, useApp, useInput } from "ink";
|
|
13
13
|
import { useTerminalSize } from "./hooks/useTerminalSize.js";
|
|
14
14
|
import { Container } from "./layout/Container.js";
|
|
15
15
|
import { Borders } from "./layout/Borders.js";
|
|
@@ -26,7 +26,8 @@ import { getStatusColor } from "./utils/utils.js";
|
|
|
26
26
|
import { KeyBindings } from "./sections/KeyBindings.js";
|
|
27
27
|
const MIN_WIDTH_WARNING = 60;
|
|
28
28
|
const SIMPLE_LAYOUT_THRESHOLD = 80;
|
|
29
|
-
const TunnelTui = ({ urls, greet, tunnelConfig }) => {
|
|
29
|
+
const TunnelTui = ({ urls, greet, tunnelConfig, disconnectInfo }) => {
|
|
30
|
+
const { exit } = useApp();
|
|
30
31
|
const { columns: terminalWidth } = useTerminalSize();
|
|
31
32
|
const isQrCodeRequested = (tunnelConfig === null || tunnelConfig === void 0 ? void 0 : tunnelConfig.qrCode) || false;
|
|
32
33
|
// States
|
|
@@ -40,6 +41,11 @@ const TunnelTui = ({ urls, greet, tunnelConfig }) => {
|
|
|
40
41
|
const { pairs } = useWebDebugger(tunnelConfig === null || tunnelConfig === void 0 ? void 0 : tunnelConfig.webDebugger);
|
|
41
42
|
const { headers, fetchHeaders, clear } = useReqResHeaders(tunnelConfig === null || tunnelConfig === void 0 ? void 0 : tunnelConfig.webDebugger);
|
|
42
43
|
const allPairs = [...pairs.values()];
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (disconnectInfo === null || disconnectInfo === void 0 ? void 0 : disconnectInfo.disconnected) {
|
|
46
|
+
exit();
|
|
47
|
+
}
|
|
48
|
+
}, [disconnectInfo, exit]);
|
|
43
49
|
// Key handling
|
|
44
50
|
useInput((input, key) => {
|
|
45
51
|
if (inDetailView && key.escape) {
|
|
@@ -95,6 +101,6 @@ const TunnelTui = ({ urls, greet, tunnelConfig }) => {
|
|
|
95
101
|
return (_jsxs(_Fragment, { children: [_jsx(Container, { children: _jsx(Borders, { children: _jsxs(Box, { flexDirection: "column", height: "100%", justifyContent: "space-between", children: [_jsxs(Box, { flexDirection: "column", children: [greet && (_jsx(Box, { justifyContent: "center", width: "94%", marginBottom: 1, children: _jsx(Text, { color: "cyanBright", bold: true, children: greet }) })), _jsxs(Box, { flexDirection: "row", justifyContent: "space-evenly", width: "100%", paddingY: 1, children: [_jsx(URLsSection, { urls: urls, currentQrIndex: currentQrIndex }), _jsx(StatsSection, { stats: stats })] }), _jsxs(Box, { flexDirection: "row", justifyContent: "space-evenly", width: "100%", paddingY: 1, children: [_jsxs(Box, { flexDirection: "column", marginBottom: 1, width: isQrCodeRequested ? "60%" : "80%", children: [_jsx(Text, { color: "yellowBright", children: "HTTP Requests:" }), visiblePairs.map((pair, i) => {
|
|
96
102
|
var _a, _b, _c, _d;
|
|
97
103
|
return (_jsx(Text, { children: _jsxs(Text, { color: selectedIndex === startIndex + i ? "cyanBright" : getStatusColor(((_a = pair.response) === null || _a === void 0 ? void 0 : _a.status) || ""), children: [selectedIndex === startIndex + i ? "> " : " ", ((_b = pair.request) === null || _b === void 0 ? void 0 : _b.method) || "", pair.response ? (_jsxs(Text, { color: selectedIndex === startIndex + i ? "cyanBright" : getStatusColor(((_c = pair.response) === null || _c === void 0 ? void 0 : _c.status) || ""), children: [" ", " ", pair.response.status] })) : (_jsx(Text, { dimColor: true, children: "..." })), " ", ((_d = pair.request) === null || _d === void 0 ? void 0 : _d.uri) || ""] }) }, i));
|
|
98
|
-
})] }), isQrCodeRequested && (_jsx(QrCodeSection, { qrCodes: qrCodes, urls: urls, currentQrIndex: currentQrIndex }))] })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop the tunnel
|
|
104
|
+
})] }), isQrCodeRequested && (_jsx(QrCodeSection, { qrCodes: qrCodes, urls: urls, currentQrIndex: currentQrIndex }))] })] }), _jsx(Box, { justifyContent: "center", marginTop: 1, children: _jsx(Text, { dimColor: true, children: "Press Ctrl+C to stop the tunnel Or press h for key bindings" }) })] }) }) }), inDetailView && (_jsx(DebuggerDetailModal, { requestText: headers === null || headers === void 0 ? void 0 : headers.req, responseText: headers === null || headers === void 0 ? void 0 : headers.res, onClose: () => setInDetailView(false) })), keyBindingView && _jsx(KeyBindings, {})] }));
|
|
99
105
|
};
|
|
100
106
|
export default TunnelTui;
|
|
@@ -25,12 +25,20 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
25
25
|
import { pinggy } from "@pinggy/pinggy";
|
|
26
26
|
import { logger } from "../logger.js";
|
|
27
27
|
import { v4 as uuidv4 } from "uuid";
|
|
28
|
+
import path from "node:path";
|
|
29
|
+
import { Worker } from "node:worker_threads";
|
|
30
|
+
import { fileURLToPath } from "node:url";
|
|
31
|
+
import CLIPrinter from "../utils/printer.js";
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = path.dirname(__filename);
|
|
28
34
|
export class TunnelManager {
|
|
29
35
|
constructor() {
|
|
30
36
|
this.tunnelsByTunnelId = new Map();
|
|
31
37
|
this.tunnelsByConfigId = new Map();
|
|
32
38
|
this.tunnelStats = new Map();
|
|
33
39
|
this.tunnelStatsListeners = new Map();
|
|
40
|
+
this.tunnelErrorListeners = new Map();
|
|
41
|
+
this.tunnelDisconnectListeners = new Map();
|
|
34
42
|
}
|
|
35
43
|
static getInstance() {
|
|
36
44
|
if (!TunnelManager.instance) {
|
|
@@ -69,9 +77,13 @@ export class TunnelManager {
|
|
|
69
77
|
instance,
|
|
70
78
|
tunnelConfig: config,
|
|
71
79
|
additionalForwarding,
|
|
80
|
+
serve: config.serve,
|
|
81
|
+
warnings: []
|
|
72
82
|
};
|
|
73
|
-
// Register stats callback for this tunnel
|
|
83
|
+
// Register stats & error callback for this tunnel
|
|
74
84
|
this.setupStatsCallback(tunnelid, managed);
|
|
85
|
+
this.setupErrorCallback(tunnelid, managed);
|
|
86
|
+
this.setupDisconnectCallback(tunnelid, managed);
|
|
75
87
|
this.tunnelsByTunnelId.set(tunnelid, managed);
|
|
76
88
|
this.tunnelsByConfigId.set(configid, managed);
|
|
77
89
|
logger.info("Tunnel created", { configid, tunnelid });
|
|
@@ -114,6 +126,9 @@ export class TunnelManager {
|
|
|
114
126
|
}
|
|
115
127
|
}
|
|
116
128
|
}
|
|
129
|
+
if (managed.serve) {
|
|
130
|
+
this.startStaticFileServer(managed);
|
|
131
|
+
}
|
|
117
132
|
return urls;
|
|
118
133
|
});
|
|
119
134
|
}
|
|
@@ -134,6 +149,10 @@ export class TunnelManager {
|
|
|
134
149
|
logger.info("Stopping tunnel", { tunnelId, configId: managed.configid });
|
|
135
150
|
try {
|
|
136
151
|
managed.instance.stop();
|
|
152
|
+
if (managed.serveWorker) {
|
|
153
|
+
logger.info("terminating serveWorker");
|
|
154
|
+
managed.serveWorker.terminate();
|
|
155
|
+
}
|
|
137
156
|
this.tunnelStats.delete(tunnelId);
|
|
138
157
|
this.tunnelStatsListeners.delete(tunnelId);
|
|
139
158
|
logger.info("Tunnel stopped", { tunnelId });
|
|
@@ -435,6 +454,34 @@ export class TunnelManager {
|
|
|
435
454
|
logger.info("Stats listener registered for tunnel", { tunnelId, listenerId });
|
|
436
455
|
return listenerId;
|
|
437
456
|
}
|
|
457
|
+
registerErrorListener(tunnelId, listener) {
|
|
458
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
459
|
+
if (!managed) {
|
|
460
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
461
|
+
}
|
|
462
|
+
if (!this.tunnelErrorListeners.has(tunnelId)) {
|
|
463
|
+
this.tunnelErrorListeners.set(tunnelId, new Map());
|
|
464
|
+
}
|
|
465
|
+
const listenerId = uuidv4();
|
|
466
|
+
const tunnelErrorListeners = this.tunnelErrorListeners.get(tunnelId);
|
|
467
|
+
tunnelErrorListeners.set(listenerId, listener);
|
|
468
|
+
logger.info("Error listener registered for tunnel", { tunnelId, listenerId });
|
|
469
|
+
return listenerId;
|
|
470
|
+
}
|
|
471
|
+
registerDisconnectListener(tunnelId, listener) {
|
|
472
|
+
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
473
|
+
if (!managed) {
|
|
474
|
+
throw new Error(`Tunnel "${tunnelId}" not found`);
|
|
475
|
+
}
|
|
476
|
+
if (!this.tunnelDisconnectListeners.has(tunnelId)) {
|
|
477
|
+
this.tunnelDisconnectListeners.set(tunnelId, new Map());
|
|
478
|
+
}
|
|
479
|
+
const listenerId = uuidv4();
|
|
480
|
+
const tunnelDisconnectListeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
481
|
+
tunnelDisconnectListeners.set(listenerId, listener);
|
|
482
|
+
logger.info("Disconnect listener registered for tunnel", { tunnelId, listenerId });
|
|
483
|
+
return listenerId;
|
|
484
|
+
}
|
|
438
485
|
/**
|
|
439
486
|
* Removes a previously registered stats listener.
|
|
440
487
|
*
|
|
@@ -459,6 +506,40 @@ export class TunnelManager {
|
|
|
459
506
|
logger.warn("Attempted to deregister non-existent stats listener", { tunnelId, listenerId });
|
|
460
507
|
}
|
|
461
508
|
}
|
|
509
|
+
deregisterErrorListener(tunnelId, listenerId) {
|
|
510
|
+
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
511
|
+
if (!listeners) {
|
|
512
|
+
logger.warn("No error listeners found for tunnel", { tunnelId });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const removed = listeners.delete(listenerId);
|
|
516
|
+
if (removed) {
|
|
517
|
+
logger.info("Error listener deregistered", { tunnelId, listenerId });
|
|
518
|
+
if (listeners.size === 0) {
|
|
519
|
+
this.tunnelErrorListeners.delete(tunnelId);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
logger.warn("Attempted to deregister non-existent error listener", { tunnelId, listenerId });
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
deregisterDisconnectListener(tunnelId, listenerId) {
|
|
527
|
+
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
528
|
+
if (!listeners) {
|
|
529
|
+
logger.warn("No disconnect listeners found for tunnel", { tunnelId });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const removed = listeners.delete(listenerId);
|
|
533
|
+
if (removed) {
|
|
534
|
+
logger.info("Disconnect listener deregistered", { tunnelId, listenerId });
|
|
535
|
+
if (listeners.size === 0) {
|
|
536
|
+
this.tunnelDisconnectListeners.delete(tunnelId);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
logger.warn("Attempted to deregister non-existent disconnect listener", { tunnelId, listenerId });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
462
543
|
getLocalserverTlsInfo(tunnelId) {
|
|
463
544
|
const managed = this.tunnelsByTunnelId.get(tunnelId);
|
|
464
545
|
if (!managed) {
|
|
@@ -494,6 +575,73 @@ export class TunnelManager {
|
|
|
494
575
|
logger.warn("Failed to set up stats callback", { tunnelId, error });
|
|
495
576
|
}
|
|
496
577
|
}
|
|
578
|
+
notifyErrorListeners(tunnelId, errorMsg, isFatal) {
|
|
579
|
+
try {
|
|
580
|
+
const listeners = this.tunnelErrorListeners.get(tunnelId);
|
|
581
|
+
if (!listeners)
|
|
582
|
+
return;
|
|
583
|
+
for (const [id, listener] of listeners) {
|
|
584
|
+
try {
|
|
585
|
+
listener(tunnelId, errorMsg, isFatal);
|
|
586
|
+
}
|
|
587
|
+
catch (err) {
|
|
588
|
+
logger.debug("Error in error-listener callback", { listenerId: id, tunnelId, err });
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
logger.debug("Failed to notify error listeners", { tunnelId, err });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
setupErrorCallback(tunnelId, managed) {
|
|
597
|
+
try {
|
|
598
|
+
const callback = (errorNo, errorMsg, recoverable) => {
|
|
599
|
+
try {
|
|
600
|
+
const msg = typeof errorMsg === "string" ? errorMsg : String(errorMsg);
|
|
601
|
+
const isFatal = true;
|
|
602
|
+
logger.debug("Tunnel reported error", { tunnelId, errorNo, errorMsg: msg, recoverable });
|
|
603
|
+
this.notifyErrorListeners(tunnelId, msg, isFatal);
|
|
604
|
+
// TODO: IF the error is fatal, we can stop the tunnel and exit.
|
|
605
|
+
}
|
|
606
|
+
catch (e) {
|
|
607
|
+
logger.warn("Error handling tunnel error callback", { tunnelId, e });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
managed.instance.setTunnelErrorCallback(callback);
|
|
611
|
+
logger.debug("Error callback set up for tunnel", { tunnelId });
|
|
612
|
+
}
|
|
613
|
+
catch (error) {
|
|
614
|
+
logger.warn("Failed to set up error callback", { tunnelId, error });
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
setupDisconnectCallback(tunnelId, managed) {
|
|
618
|
+
try {
|
|
619
|
+
const callback = (error, messages) => {
|
|
620
|
+
try {
|
|
621
|
+
logger.debug("Tunnel disconnected", { tunnelId, error, messages });
|
|
622
|
+
const listeners = this.tunnelDisconnectListeners.get(tunnelId);
|
|
623
|
+
if (!listeners)
|
|
624
|
+
return;
|
|
625
|
+
for (const [id, listener] of listeners) {
|
|
626
|
+
try {
|
|
627
|
+
listener(tunnelId, error, messages);
|
|
628
|
+
}
|
|
629
|
+
catch (err) {
|
|
630
|
+
logger.debug("Error in disconnect-listener callback", { listenerId: id, tunnelId, err });
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
catch (e) {
|
|
635
|
+
logger.warn("Error handling tunnel disconnect callback", { tunnelId, e });
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
managed.instance.setTunnelDisconnectedCallback(callback);
|
|
639
|
+
logger.debug("Disconnect callback set up for tunnel", { tunnelId });
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
logger.warn("Failed to set up disconnect callback", { tunnelId, error });
|
|
643
|
+
}
|
|
644
|
+
}
|
|
497
645
|
/**
|
|
498
646
|
* Updates the stored stats for a tunnel and notifies all registered listeners.
|
|
499
647
|
*/
|
|
@@ -548,4 +696,42 @@ export class TunnelManager {
|
|
|
548
696
|
const parsed = typeof value === 'number' ? value : parseInt(String(value), 10);
|
|
549
697
|
return isNaN(parsed) ? 0 : parsed;
|
|
550
698
|
}
|
|
699
|
+
startStaticFileServer(managed) {
|
|
700
|
+
var _a;
|
|
701
|
+
try {
|
|
702
|
+
const fileServerWorkerPath = path.resolve(__dirname, "../workers/file_serve_worker.js");
|
|
703
|
+
const staticServerWorker = new Worker(fileServerWorkerPath, {
|
|
704
|
+
workerData: {
|
|
705
|
+
dir: managed.serve,
|
|
706
|
+
port: (_a = managed.tunnelConfig) === null || _a === void 0 ? void 0 : _a.forwarding,
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
staticServerWorker.on("message", (msg) => {
|
|
710
|
+
var _a, _b;
|
|
711
|
+
switch (msg.type) {
|
|
712
|
+
case "started":
|
|
713
|
+
logger.info("Static file server started", { dir: managed.serve });
|
|
714
|
+
break;
|
|
715
|
+
case "warning":
|
|
716
|
+
if (msg.code === "INVALID_TUNNEL_SERVE_PATH") {
|
|
717
|
+
managed.warnings = (_a = managed.warnings) !== null && _a !== void 0 ? _a : [];
|
|
718
|
+
managed.warnings.push({ code: msg.code, message: msg.message });
|
|
719
|
+
}
|
|
720
|
+
CLIPrinter.warn(msg.message);
|
|
721
|
+
break;
|
|
722
|
+
case "error":
|
|
723
|
+
managed.warnings = (_b = managed.warnings) !== null && _b !== void 0 ? _b : [];
|
|
724
|
+
managed.warnings.push({
|
|
725
|
+
code: "UNKNOWN_WARNING",
|
|
726
|
+
message: msg.message,
|
|
727
|
+
});
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
});
|
|
731
|
+
managed.serveWorker = staticServerWorker;
|
|
732
|
+
}
|
|
733
|
+
catch (error) {
|
|
734
|
+
logger.error("Error starting static file server", error);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
551
737
|
}
|
package/dist/types.js
CHANGED
|
@@ -21,6 +21,7 @@ export var TunnelErrorCodeType;
|
|
|
21
21
|
export var TunnelWarningCode;
|
|
22
22
|
(function (TunnelWarningCode) {
|
|
23
23
|
TunnelWarningCode["InvalidTunnelServePath"] = "INVALID_TUNNEL_SERVE_PATH";
|
|
24
|
+
TunnelWarningCode["UnknownWarning"] = "UNKNOWN_WARNING";
|
|
24
25
|
})(TunnelWarningCode || (TunnelWarningCode = {}));
|
|
25
26
|
export const ErrorCode = {
|
|
26
27
|
InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
import { createServer } from "http";
|
|
11
|
+
import { readFile, readdir, stat } from "fs/promises";
|
|
12
|
+
import { existsSync } from "fs";
|
|
13
|
+
import { extname, join, resolve, relative } from "path";
|
|
14
|
+
import { URL } from "url";
|
|
15
|
+
import mime from "mime";
|
|
16
|
+
import { logger } from "../logger.js";
|
|
17
|
+
import { directoryListingHtml, invalidPathErrorHtml } from "./htmlTemplates.js";
|
|
18
|
+
export class FileServerError extends Error {
|
|
19
|
+
constructor(message, code) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = "FileServerError";
|
|
22
|
+
this.code = code;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export function startFileServer(dirPath_1) {
|
|
26
|
+
return __awaiter(this, arguments, void 0, function* (dirPath, port = 8080) {
|
|
27
|
+
let invalidPathError = null;
|
|
28
|
+
const root = resolve(dirPath);
|
|
29
|
+
logger.debug("Starting file server with root:", root, "on port:", port);
|
|
30
|
+
if (!existsSync(root)) {
|
|
31
|
+
logger.debug("Invalid root path for file server:", root);
|
|
32
|
+
invalidPathError = new FileServerError(`The path ${dirPath} does not exist. Please check the path and try again.`, "INVALID_TUNNEL_SERVE_PATH");
|
|
33
|
+
}
|
|
34
|
+
const server = createServer((req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
try {
|
|
36
|
+
// If invalid root, show an HTML error page
|
|
37
|
+
if (invalidPathError) {
|
|
38
|
+
const html = invalidPathErrorHtml(invalidPathError);
|
|
39
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
40
|
+
res.end(html);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const reqUrl = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
44
|
+
let filePath = join(root, decodeURIComponent(reqUrl.pathname));
|
|
45
|
+
let stats;
|
|
46
|
+
try {
|
|
47
|
+
stats = yield stat(filePath);
|
|
48
|
+
}
|
|
49
|
+
catch (_a) {
|
|
50
|
+
res.statusCode = 404;
|
|
51
|
+
res.end("404 Not Found");
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (stats.isDirectory() && !reqUrl.pathname.endsWith("/")) {
|
|
55
|
+
res.writeHead(301, { Location: reqUrl.pathname + "/" });
|
|
56
|
+
res.end();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
// Directory handling
|
|
60
|
+
if (stats.isDirectory()) {
|
|
61
|
+
const indexPath = join(filePath, "index.html");
|
|
62
|
+
if (existsSync(indexPath)) {
|
|
63
|
+
filePath = indexPath;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
// No index.html — show directory listing
|
|
67
|
+
const items = yield readdir(filePath, { withFileTypes: true });
|
|
68
|
+
const list = items
|
|
69
|
+
.map((item) => {
|
|
70
|
+
// Get the display name
|
|
71
|
+
const name = item.name + (item.isDirectory() ? "/" : "");
|
|
72
|
+
// Construct the current base URL
|
|
73
|
+
const base = new URL(reqUrl.pathname, `http://${req.headers.host}`);
|
|
74
|
+
// Create the full URL
|
|
75
|
+
const hrefUrl = new URL(encodeURIComponent(name), base);
|
|
76
|
+
return `<li><a href="${hrefUrl.pathname}">${name}</a></li>`;
|
|
77
|
+
})
|
|
78
|
+
.join("");
|
|
79
|
+
const relativePath = relative(root, filePath) || "/";
|
|
80
|
+
const html = directoryListingHtml(relativePath, list);
|
|
81
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
82
|
+
res.end(html);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Normal file serving
|
|
87
|
+
const content = yield readFile(filePath);
|
|
88
|
+
const type = mime.getType(extname(filePath)) || "application/octet-stream";
|
|
89
|
+
res.writeHead(200, { "Content-Type": type });
|
|
90
|
+
res.end(content);
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
logger.debug("Error in handling request", err);
|
|
94
|
+
res.statusCode = 500;
|
|
95
|
+
res.end(`Internal Server Error: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}));
|
|
98
|
+
yield new Promise((resolve, reject) => {
|
|
99
|
+
server.listen(port, () => {
|
|
100
|
+
resolve();
|
|
101
|
+
});
|
|
102
|
+
server.on("error", (err) => {
|
|
103
|
+
logger.debug("Error starting file server", err);
|
|
104
|
+
reject(err);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
hasInvalidPath: !!invalidPathError,
|
|
109
|
+
error: invalidPathError ? { message: invalidPathError.message, code: invalidPathError.code } : null
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
}
|