pinggy 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +182 -0
  3. package/dist/TunnelConfig.js +39 -0
  4. package/dist/TunnelManager.js +155 -0
  5. package/dist/cli/buildConfig.js +323 -0
  6. package/dist/cli/defaults.js +18 -0
  7. package/dist/cli/extendedOptions.js +134 -0
  8. package/dist/cli/help.js +37 -0
  9. package/dist/cli/options.js +36 -0
  10. package/dist/cli/remoteManagement.js +167 -0
  11. package/dist/cli/starCli.js +62 -0
  12. package/dist/cli/websocket_handlers.js +160 -0
  13. package/dist/index.js +60 -0
  14. package/dist/logger.js +67 -0
  15. package/dist/remote_management/handler.js +182 -0
  16. package/dist/remote_management/remoteManagement.js +159 -0
  17. package/dist/remote_management/remote_schema.js +122 -0
  18. package/dist/remote_management/websocket_handlers.js +163 -0
  19. package/dist/tui/asciArt.js +7 -0
  20. package/dist/tui/index.js +75 -0
  21. package/dist/tui/useTerminalSize.js +21 -0
  22. package/dist/tunnel_manager/TunnelManager.js +551 -0
  23. package/dist/tunnel_manager/handler.js +102 -0
  24. package/dist/tunnel_manager/remote_schema.js +106 -0
  25. package/dist/types.js +105 -0
  26. package/dist/utils/parseArgs.js +7 -0
  27. package/dist/utils/printer.js +68 -0
  28. package/dist/utils/util.js +3 -0
  29. package/jest.config.js +19 -0
  30. package/package.json +49 -0
  31. package/src/TunnelConfig.ts +40 -0
  32. package/src/_tests_/build_config.test.ts +91 -0
  33. package/src/cli/buildConfig.ts +357 -0
  34. package/src/cli/defaults.ts +20 -0
  35. package/src/cli/extendedOptions.ts +134 -0
  36. package/src/cli/help.ts +41 -0
  37. package/src/cli/options.ts +46 -0
  38. package/src/cli/starCli.tsx +118 -0
  39. package/src/cli/worker.ts +72 -0
  40. package/src/index.ts +65 -0
  41. package/src/logger.ts +86 -0
  42. package/src/remote_management/handler.ts +199 -0
  43. package/src/remote_management/remoteManagement.ts +168 -0
  44. package/src/remote_management/remote_schema.ts +134 -0
  45. package/src/remote_management/websocket_handlers.ts +166 -0
  46. package/src/tui/asciArt.ts +7 -0
  47. package/src/tui/hooks/useQrCodes.ts +27 -0
  48. package/src/tui/hooks/useReqResHeaders.ts +27 -0
  49. package/src/tui/hooks/useTerminalSize.ts +26 -0
  50. package/src/tui/hooks/useTerminalStats.ts +24 -0
  51. package/src/tui/hooks/useWebDebugger.ts +98 -0
  52. package/src/tui/index.tsx +221 -0
  53. package/src/tui/layout/Borders.tsx +15 -0
  54. package/src/tui/layout/Container.tsx +15 -0
  55. package/src/tui/sections/DebuggerDetailModal.tsx +53 -0
  56. package/src/tui/sections/KeyBindings.tsx +58 -0
  57. package/src/tui/sections/QrCodeSection.tsx +28 -0
  58. package/src/tui/sections/StatsSection.tsx +20 -0
  59. package/src/tui/sections/URLsSection.tsx +53 -0
  60. package/src/tui/utils/utils.ts +35 -0
  61. package/src/tunnel_manager/TunnelManager.ts +646 -0
  62. package/src/types.ts +234 -0
  63. package/src/utils/getFreePort.ts +41 -0
  64. package/src/utils/parseArgs.ts +29 -0
  65. package/src/utils/printer.ts +79 -0
  66. package/src/utils/util.ts +13 -0
  67. package/tsconfig.jest.json +8 -0
  68. package/tsconfig.json +17 -0
package/src/types.ts ADDED
@@ -0,0 +1,234 @@
1
+ import { PinggyOptions, TunnelUsageType } from "@pinggy/pinggy";
2
+
3
+ // Local representation of additional forwarding
4
+ export interface AdditionalForwarding {
5
+ remoteDomain?: string;
6
+ remotePort: number;
7
+ localDomain: string;
8
+ localPort: number;
9
+ }
10
+
11
+ export interface TunnelStatus {
12
+ tunnelid: string,
13
+ remoteurls: string[],
14
+ tunnelconfig: PinggyOptions,
15
+ status: Status,
16
+ stats: TunnelUsageType
17
+ }
18
+
19
+
20
+
21
+ // Enum for TunnelStateType
22
+ export enum TunnelStateType {
23
+ New = "idle",
24
+ Starting = "starting",
25
+ Running = "running",
26
+ Live = "live",
27
+ Closed = "closed",
28
+ Exited = "exited"
29
+ }
30
+
31
+ // Enum for TunnelErrorCodeType
32
+ export enum TunnelErrorCodeType {
33
+ NonResponsive = "non_responsive",
34
+ FailedToConnect = "failed_to_connect",
35
+ ErrorInAdditionalForwarding = "additional_forwarding_error",
36
+ WebdebuggerError = "webdebugger_error",
37
+ NoError = ""
38
+ }
39
+
40
+ // Enum for TunnelWarningCode
41
+ export enum TunnelWarningCode {
42
+ InvalidTunnelServePath = "INVALID_TUNNEL_SERVE_PATH"
43
+ }
44
+
45
+ // Interface for Warning
46
+ export interface Warning {
47
+ code: TunnelWarningCode;
48
+ message: string;
49
+ }
50
+
51
+
52
+
53
+ // Main Status interface
54
+ export interface Status {
55
+ state: TunnelStateType;
56
+ errorcode: TunnelErrorCodeType;
57
+ errormsg: string;
58
+ createdtimestamp: Date;
59
+ starttimestamp: Date;
60
+ endtimestamp: Date;
61
+ warnings: Warning[];
62
+ }
63
+
64
+ export type Forwarding = {
65
+ remoteDomain?: string;
66
+ remotePort: number;
67
+ localDomain: string;
68
+ localPort: number;
69
+ };
70
+
71
+ export type FinalConfig = (PinggyOptions & { configid: string }) & {
72
+ conf?: string;
73
+ saveconf?: string;
74
+ serve?: string;
75
+ remoteManagement?: string;
76
+ additionalForwarding?: Forwarding[];
77
+ manage?: string;
78
+ version?: boolean;
79
+ NoTUI?: boolean;
80
+ qrCode?: boolean;
81
+ };
82
+
83
+ export type ErrorCodeType =
84
+ | "INVALID_REQUEST_METHOD"
85
+ | "COULD_NOT_READ_BODY"
86
+ | "INTERNAL_SERVER_ERROR"
87
+ | "INVALID_DATA_FORMAT"
88
+ | "ERROR_STARTING_TUNNEL"
89
+ | "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND"
90
+ | "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING"
91
+ | "WEBSOCKET_UPGRADE_FAILED"
92
+ | "REMOTE_MANAGEMENT_ALREADY_RUNNING"
93
+ | "REMOTE_MANAGEMENT_NOT_RUNNING"
94
+ | "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED";
95
+
96
+ export const ErrorCode: Record<string, ErrorCodeType> = {
97
+ InvalidRequestMethodError: "INVALID_REQUEST_METHOD",
98
+ InvalidRequestBodyError: "COULD_NOT_READ_BODY",
99
+ InternalServerError: "INTERNAL_SERVER_ERROR",
100
+ InvalidBodyFormatError: "INVALID_DATA_FORMAT",
101
+ ErrorStartingTunnel: "ERROR_STARTING_TUNNEL",
102
+ TunnelNotFound: "TUNNEL_WITH_ID_OR_CONFIG_ID_NOT_FOUND",
103
+ TunnelAlreadyRunningError: "TUNNEL_WITH_ID_OR_CONFIG_ID_ALREADY_RUNNING",
104
+ WebsocketUpgradeFailError: "WEBSOCKET_UPGRADE_FAILED",
105
+ RemoteManagementAlreadyRunning: "REMOTE_MANAGEMENT_ALREADY_RUNNING",
106
+ RemoteManagementNotRunning: "REMOTE_MANAGEMENT_NOT_RUNNING",
107
+ RemoteManagementDeserializationFailed: "REMOTE_MANAGEMENT_DESERIALIZATION_FAILED",
108
+ } as const;
109
+
110
+ export interface ErrorResponse {
111
+ code: ErrorCodeType;
112
+ message: string;
113
+ }
114
+
115
+ export function isErrorResponse(obj: unknown): obj is ErrorResponse {
116
+ return (
117
+ typeof obj === 'object' &&
118
+ obj !== null &&
119
+ 'code' in obj &&
120
+ 'message' in obj &&
121
+ typeof (obj as ErrorResponse).message === 'string' &&
122
+ Object.values(ErrorCode).includes((obj as ErrorResponse).code)
123
+ );
124
+ }
125
+
126
+ export function newErrorResponse(errorResponse: ErrorResponse): ErrorResponse;
127
+ export function newErrorResponse(code: ErrorCodeType, message: string): ErrorResponse;
128
+ export function newErrorResponse(codeOrError: ErrorCodeType | ErrorResponse, message?: string): ErrorResponse {
129
+ if (typeof codeOrError === 'object') {
130
+ return codeOrError;
131
+ }
132
+ return {
133
+ code: codeOrError,
134
+ message: message!
135
+ };
136
+ }
137
+
138
+
139
+ export interface ResponseObj {
140
+ response: Uint8Array;
141
+ requestid: string;
142
+ command: string;
143
+ error: boolean;
144
+ errorresponse: ErrorResponse;
145
+ }
146
+
147
+ export function NewResponseObject(data: unknown): ResponseObj {
148
+ const encoder = new TextEncoder();
149
+ const bytes = encoder.encode(JSON.stringify(data));
150
+ return {
151
+ response: bytes,
152
+ requestid: "",
153
+ command: "",
154
+ error: false,
155
+ errorresponse: {} as ErrorResponse,
156
+ };
157
+ }
158
+
159
+ export function NewErrorResponseObject(errorResponse: ErrorResponse): ResponseObj {
160
+ return {
161
+ response: new Uint8Array(),
162
+ requestid: "",
163
+ command: "",
164
+ error: true,
165
+ errorresponse: errorResponse,
166
+ };
167
+ }
168
+
169
+ export function newStatus(
170
+ tunnelState: TunnelStateType,
171
+ errorCode: TunnelErrorCodeType,
172
+ errorMsg: string,
173
+ ): Status {
174
+ let assignedState = tunnelState;
175
+ if (tunnelState === TunnelStateType.Live) {
176
+ assignedState = TunnelStateType.Running;
177
+ } else if (tunnelState === TunnelStateType.New) {
178
+ assignedState = TunnelStateType.Starting;
179
+ } else if (tunnelState === TunnelStateType.Closed) {
180
+ assignedState = TunnelStateType.Exited;
181
+ }
182
+
183
+ return {
184
+ state: assignedState,
185
+ errorcode: errorCode,
186
+ errormsg: errorMsg,
187
+ createdtimestamp: new Date(),
188
+ starttimestamp: new Date(),
189
+ endtimestamp: new Date(),
190
+ warnings: []
191
+ };
192
+ }
193
+
194
+ export function newStats(): TunnelUsageType {
195
+ return {
196
+ numLiveConnections: 0,
197
+ numTotalConnections: 0,
198
+ numTotalReqBytes: 0,
199
+ numTotalResBytes: 0,
200
+ numTotalTxBytes: 0,
201
+ elapsedTime: 0,
202
+ };
203
+ }
204
+
205
+ export interface Request {
206
+ key: number;
207
+ method: string;
208
+ uri: string;
209
+ }
210
+
211
+ export interface Response {
212
+ key: number;
213
+ status: string;
214
+ }
215
+
216
+ export interface ReqResPair {
217
+ request: Request;
218
+ response: Response;
219
+ reqHeaders: string;
220
+ resHeaders: string;
221
+ headersLoaded: boolean;
222
+ }
223
+ export interface StatsAll {
224
+ activeConn: number;
225
+ numRequests: number;
226
+ numResponses: number;
227
+ reqBytes: number;
228
+ resBytes: number;
229
+ totalConn: number;
230
+ }
231
+ export interface WebDebuggerSocketRequest {
232
+ Req: Request;
233
+ Res: Response;
234
+ }
@@ -0,0 +1,41 @@
1
+ import net from "net";
2
+
3
+ /**
4
+ * Get a free TCP port.
5
+ * If `providedPort` is available, returns that.
6
+ * Otherwise, returns a randomly assigned free port from the OS.
7
+ */
8
+ export function getFreePort(webDebugger: string): Promise<number> {
9
+ return new Promise((resolve, reject) => {
10
+ // Try provided port first
11
+ const tryPort = (portToTry: number) => {
12
+ const server = net.createServer();
13
+
14
+ server.unref();
15
+
16
+ server.on("error", (err) => {
17
+ // If provided port failed, try random port (0)
18
+ if (portToTry !== 0) {
19
+ tryPort(0);
20
+ } else {
21
+ reject(err);
22
+ }
23
+ });
24
+
25
+ server.listen(portToTry, () => {
26
+ const address: net.AddressInfo = server.address() as net.AddressInfo;
27
+ const port = address ? address.port : 0;
28
+ server.close(() => resolve(port));
29
+ });
30
+ };
31
+ let providedPort = 0;
32
+ if (webDebugger && webDebugger.includes(":")) {
33
+ const portPart = webDebugger.split(":")[1];
34
+ const parsed = parseInt(portPart, 10);
35
+ if (!isNaN(parsed) && parsed > 0 && parsed < 65536) {
36
+ providedPort = parsed;
37
+ }
38
+ }
39
+ tryPort(providedPort);
40
+ });
41
+ }
@@ -0,0 +1,29 @@
1
+ import { parseArgs } from "util";
2
+ import { cliOptions } from "../cli/options.js";
3
+
4
+ export type OptionSpec = {
5
+ type: 'string' | 'boolean';
6
+ multiple?: boolean;
7
+ short?: string;
8
+ description?: string;
9
+ hidden?: boolean;
10
+ };
11
+
12
+ export type CliOptions = typeof cliOptions;
13
+
14
+ export type ParsedValues<T extends Record<string, OptionSpec>> = {
15
+ [K in keyof T]:
16
+ T[K]['type'] extends 'string'
17
+ ? (T[K]['multiple'] extends true ? string[] : string | undefined)
18
+ : (T[K]['type'] extends 'boolean' ? boolean | undefined : never);
19
+ };
20
+
21
+ export function parseCliArgs<T extends Record<string, OptionSpec>>(options: T) {
22
+ return parseArgs({
23
+ options,
24
+ allowPositionals: true,
25
+ }) as unknown as {
26
+ values: ParsedValues<T>;
27
+ positionals: string[];
28
+ };
29
+ }
@@ -0,0 +1,79 @@
1
+ import chalk from "chalk";
2
+ import ora, {Ora} from "ora";
3
+
4
+ interface CLIErrorDefinition {
5
+ match: (err: unknown) => boolean;
6
+ message: (err: unknown) => string;
7
+ }
8
+
9
+ class CLIPrinter {
10
+ private static spinner: Ora | null = null;
11
+ private static isCLIError(err: unknown): err is Error & { code?: string; option?: string; value?: string } {
12
+ return err instanceof Error;
13
+ }
14
+ private static errorDefinitions: CLIErrorDefinition[] = [
15
+ {
16
+ match: (err) => this.isCLIError(err) && err.code === "ERR_PARSE_ARGS_UNKNOWN_OPTION",
17
+ message: (err) => {
18
+ const match = /Unknown option '(.+?)'/.exec((err as any).message);
19
+ const option = match ? match[1] : '(unknown)';
20
+ return `Unknown option '${option}'. Please check your command or use pinggy --h for guidance.`;
21
+ },
22
+ },
23
+ {
24
+ match: (err) => this.isCLIError(err) && err.code === "ERR_PARSE_ARGS_MISSING_OPTION_VALUE",
25
+ message: (err) => `Missing required argument for option '${(err as any).option}'.`,
26
+ },
27
+ {
28
+ match: (err) => this.isCLIError(err) && err.code === "ERR_PARSE_ARGS_INVALID_OPTION_VALUE",
29
+ message: (err) => `Invalid argument'${(err as any).message}'.`,
30
+ },
31
+ {
32
+ match: (err) => this.isCLIError(err) && err.code === "ENOENT",
33
+ message: (err) => `File or directory not found: ${(err as any).message}`,
34
+ },
35
+ {
36
+ match: () => true, // fallback
37
+ message: (err) => (this.isCLIError(err) ? err.message : String(err)),
38
+ },
39
+ ];
40
+
41
+ static print(message: string, ...args: any[]) {
42
+ console.log(message, ...args);
43
+ }
44
+
45
+ static error(err: unknown) {
46
+ const def = this.errorDefinitions.find((d) => d.match(err))!;
47
+ const msg = def.message(err);
48
+ console.error(chalk.redBright("✖ Error:"), chalk.red(msg));
49
+ process.exit(1);
50
+ }
51
+
52
+ static warn(message: string) {
53
+ console.warn(chalk.yellowBright("⚠ Warning:"), chalk.yellow(message));
54
+ }
55
+
56
+ static success(message: string) {
57
+ console.log(chalk.greenBright("✔ Success:"), chalk.green(message));
58
+ }
59
+ static info(message: string) {
60
+ console.log(chalk.blue(message));
61
+ }
62
+
63
+
64
+ static startSpinner(message: string) {
65
+ this.spinner = ora({ text: message, color: "cyan" }).start();
66
+ }
67
+
68
+ static stopSpinnerSuccess(message: string) {
69
+ this.spinner?.succeed(message);
70
+ this.spinner = null;
71
+ }
72
+
73
+ static stopSpinnerFail(message: string) {
74
+ this.spinner?.fail(message);
75
+ this.spinner = null;
76
+ }
77
+ }
78
+
79
+ export default CLIPrinter;
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'fs';
2
+ import { createRequire } from 'module';
3
+
4
+ export function isValidPort(p: number): boolean {
5
+ return Number.isInteger(p) && p > 0 && p < 65536;
6
+ }
7
+
8
+ const require = createRequire(import.meta.url);
9
+ const pkg = require('../../package.json');
10
+
11
+ export function getVersion(): string {
12
+ return pkg.version ?? '';
13
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["node", "jest"],
5
+ "noEmit": true
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2016",
4
+ "module": "nodenext",
5
+ "moduleResolution": "nodenext",
6
+ "esModuleInterop": true,
7
+ "forceConsistentCasingInFileNames": true,
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "outDir": "dist",
11
+ "lib": ["es2017", "dom"],
12
+ "types": ["node"],
13
+ "jsx": "react-jsx",
14
+ },
15
+ "include": ["src/**/*"],
16
+ "exclude": ["node_modules", "**/*.test.ts"]
17
+ }