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.
@@ -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.NoTUI || false, qrCode: qrCode || false });
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;
@@ -9,7 +9,7 @@ export const defaultOptions = {
9
9
  basicAuth: [],
10
10
  bearerTokenAuth: [],
11
11
  headerModification: [],
12
- force: true,
12
+ force: false,
13
13
  xForwardedFor: false,
14
14
  httpsOnly: false,
15
15
  originalRequestUrl: false,
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(" pinggy [options] [user@domain] # Domain can be any valid domain\n");
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)\n");
25
- console.log("Examples (SSH-style):");
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
  }
@@ -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
- NoTUI: { type: 'boolean', description: 'Disable TUI in remote management mode' },
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
@@ -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("./dist/cli/worker.js");
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, _f;
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(TunnelTui, { urls: (_e = TunnelData.urls) !== null && _e !== void 0 ? _e : [], greet: (_f = TunnelData.greet) !== null && _f !== void 0 ? _f : "", tunnelConfig: finalConfig }));
80
- yield tui.start();
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
- CLIPrinter.error(`Worker thread error: ${err}`);
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." }) })] }) }) }), 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, {})] }));
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
+ }