api-response-manager 2.5.2 → 2.5.4
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 +1 -1
- package/bin/arm.js +22 -1
- package/commands/account.js +23 -11
- package/commands/install.js +136 -0
- package/commands/login.js +25 -0
- package/commands/tunnel.js +233 -17
- package/commands/uninstall.js +153 -0
- package/package.json +28 -3
- package/utils/api.js +5 -0
package/README.md
CHANGED
package/bin/arm.js
CHANGED
|
@@ -24,6 +24,8 @@ const healthCommand = require('../commands/health');
|
|
|
24
24
|
const gatewayCommand = require('../commands/gateway');
|
|
25
25
|
const ingressCommand = require('../commands/ingress');
|
|
26
26
|
const accountCommand = require('../commands/account');
|
|
27
|
+
const installCommand = require('../commands/install');
|
|
28
|
+
const uninstallCommand = require('../commands/uninstall');
|
|
27
29
|
|
|
28
30
|
// CLI setup
|
|
29
31
|
program
|
|
@@ -38,6 +40,7 @@ program
|
|
|
38
40
|
.option('-e, --email <email>', 'Email address')
|
|
39
41
|
.option('-p, --password <password>', 'Password')
|
|
40
42
|
.option('--provider <provider>', 'OAuth provider (google, github, microsoft)')
|
|
43
|
+
.option('-t, --token <token>', 'API token for CI/CD automation')
|
|
41
44
|
.action(loginCommand);
|
|
42
45
|
|
|
43
46
|
// Logout command
|
|
@@ -55,9 +58,10 @@ program
|
|
|
55
58
|
.option('-n, --name <name>', 'Tunnel name')
|
|
56
59
|
.option('-a, --auth', 'Enable basic authentication')
|
|
57
60
|
.option('-r, --rate-limit <limit>', 'Rate limit (requests per minute)', '60')
|
|
58
|
-
.option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ws, wss)', 'http')
|
|
61
|
+
.option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ssh, ws, wss)', 'http')
|
|
59
62
|
.option('--ssl', 'Enable SSL/HTTPS')
|
|
60
63
|
.option('-d, --domain <domain>', 'Custom domain')
|
|
64
|
+
.option('--json', 'Output in JSON format (for CI/CD automation)')
|
|
61
65
|
.action(tunnelCommand.start);
|
|
62
66
|
|
|
63
67
|
program
|
|
@@ -71,6 +75,11 @@ program
|
|
|
71
75
|
.argument('<tunnelId>', 'Tunnel ID to stop')
|
|
72
76
|
.action(tunnelCommand.stop);
|
|
73
77
|
|
|
78
|
+
program
|
|
79
|
+
.command('tunnel:stop-all')
|
|
80
|
+
.description('Stop all active tunnels')
|
|
81
|
+
.action(tunnelCommand.stopAll);
|
|
82
|
+
|
|
74
83
|
program
|
|
75
84
|
.command('tunnel:logs')
|
|
76
85
|
.description('View tunnel request logs')
|
|
@@ -422,6 +431,18 @@ program
|
|
|
422
431
|
.argument('<yamlFile>', 'Path to YAML file')
|
|
423
432
|
.action(ingressCommand.validate);
|
|
424
433
|
|
|
434
|
+
// Install command (self-install with PATH configuration)
|
|
435
|
+
program
|
|
436
|
+
.command('install')
|
|
437
|
+
.description('Install CLI to system and configure PATH')
|
|
438
|
+
.action(installCommand);
|
|
439
|
+
|
|
440
|
+
// Uninstall command (remove CLI and PATH configuration)
|
|
441
|
+
program
|
|
442
|
+
.command('uninstall')
|
|
443
|
+
.description('Uninstall CLI from system and remove from PATH')
|
|
444
|
+
.action(uninstallCommand);
|
|
445
|
+
|
|
425
446
|
// Account commands
|
|
426
447
|
program
|
|
427
448
|
.command('account')
|
package/commands/account.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const chalk = require('chalk');
|
|
2
2
|
const ora = require('ora');
|
|
3
3
|
const Table = require('cli-table3');
|
|
4
|
-
const
|
|
4
|
+
const api = require('../utils/api');
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Get current user info
|
|
@@ -10,10 +10,14 @@ async function info() {
|
|
|
10
10
|
const spinner = ora('Fetching account info...').start();
|
|
11
11
|
|
|
12
12
|
try {
|
|
13
|
-
const
|
|
14
|
-
|
|
13
|
+
const token = api.getToken();
|
|
14
|
+
if (!token) {
|
|
15
|
+
spinner.fail('Not authenticated');
|
|
16
|
+
console.error(chalk.red('Please run: arm login'));
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
15
19
|
|
|
16
|
-
const response = await api.get('/auth/me');
|
|
20
|
+
const response = await api.client.get('/auth/me');
|
|
17
21
|
const user = response.data.user;
|
|
18
22
|
|
|
19
23
|
spinner.stop();
|
|
@@ -52,10 +56,14 @@ async function features() {
|
|
|
52
56
|
const spinner = ora('Fetching feature access...').start();
|
|
53
57
|
|
|
54
58
|
try {
|
|
55
|
-
const
|
|
56
|
-
|
|
59
|
+
const token = api.getToken();
|
|
60
|
+
if (!token) {
|
|
61
|
+
spinner.fail('Not authenticated');
|
|
62
|
+
console.error(chalk.red('Please run: arm login'));
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
57
65
|
|
|
58
|
-
const response = await api.get('/auth/features');
|
|
66
|
+
const response = await api.client.get('/auth/features');
|
|
59
67
|
const { tier, features } = response.data;
|
|
60
68
|
|
|
61
69
|
spinner.stop();
|
|
@@ -102,12 +110,16 @@ async function usage() {
|
|
|
102
110
|
const spinner = ora('Fetching usage stats...').start();
|
|
103
111
|
|
|
104
112
|
try {
|
|
105
|
-
const
|
|
106
|
-
|
|
113
|
+
const token = api.getToken();
|
|
114
|
+
if (!token) {
|
|
115
|
+
spinner.fail('Not authenticated');
|
|
116
|
+
console.error(chalk.red('Please run: arm login'));
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
107
119
|
|
|
108
120
|
const [userRes, featuresRes] = await Promise.all([
|
|
109
|
-
api.get('/auth/me'),
|
|
110
|
-
api.get('/auth/features')
|
|
121
|
+
api.client.get('/auth/me'),
|
|
122
|
+
api.client.get('/auth/features')
|
|
111
123
|
]);
|
|
112
124
|
|
|
113
125
|
const user = userRes.data.user;
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const install = async () => {
|
|
8
|
+
const platform = os.platform();
|
|
9
|
+
const currentPath = process.execPath;
|
|
10
|
+
|
|
11
|
+
console.log('');
|
|
12
|
+
console.log(chalk.cyan(' TunnelAPI CLI Installer'));
|
|
13
|
+
console.log(chalk.cyan(' ======================='));
|
|
14
|
+
console.log('');
|
|
15
|
+
|
|
16
|
+
// Determine if running as standalone binary or via Node
|
|
17
|
+
const isStandalone = !currentPath.includes('node');
|
|
18
|
+
|
|
19
|
+
if (!isStandalone) {
|
|
20
|
+
console.log(chalk.yellow(' You are running via npm/Node.js.'));
|
|
21
|
+
console.log(chalk.yellow(' The CLI is already installed globally via npm.'));
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(' To verify: ' + chalk.white('arm --version'));
|
|
24
|
+
console.log('');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
if (platform === 'win32') {
|
|
30
|
+
await installWindows(currentPath);
|
|
31
|
+
} else if (platform === 'darwin' || platform === 'linux') {
|
|
32
|
+
await installUnix(currentPath, platform);
|
|
33
|
+
} else {
|
|
34
|
+
console.log(chalk.red(` Unsupported platform: ${platform}`));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
} catch (error) {
|
|
38
|
+
console.log(chalk.red(` Installation failed: ${error.message}`));
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
async function installWindows(currentPath) {
|
|
44
|
+
const installDir = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'TunnelAPI');
|
|
45
|
+
const targetPath = path.join(installDir, 'arm.exe');
|
|
46
|
+
|
|
47
|
+
console.log(chalk.yellow('[1/3] Creating install directory...'));
|
|
48
|
+
if (!fs.existsSync(installDir)) {
|
|
49
|
+
fs.mkdirSync(installDir, { recursive: true });
|
|
50
|
+
}
|
|
51
|
+
console.log(chalk.gray(` ${installDir}`));
|
|
52
|
+
|
|
53
|
+
console.log(chalk.yellow('[2/3] Copying binary...'));
|
|
54
|
+
if (currentPath !== targetPath) {
|
|
55
|
+
fs.copyFileSync(currentPath, targetPath);
|
|
56
|
+
}
|
|
57
|
+
console.log(chalk.green(' Copied successfully'));
|
|
58
|
+
|
|
59
|
+
console.log(chalk.yellow('[3/3] Configuring PATH...'));
|
|
60
|
+
try {
|
|
61
|
+
// Get current user PATH
|
|
62
|
+
const currentUserPath = execSync('powershell -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"', { encoding: 'utf8' }).trim();
|
|
63
|
+
|
|
64
|
+
if (!currentUserPath.includes(installDir)) {
|
|
65
|
+
// Add to user PATH
|
|
66
|
+
const newPath = currentUserPath ? `${currentUserPath};${installDir}` : installDir;
|
|
67
|
+
execSync(`powershell -Command "[Environment]::SetEnvironmentVariable('Path', '${newPath.replace(/'/g, "''")}', 'User')"`, { encoding: 'utf8' });
|
|
68
|
+
console.log(chalk.green(' Added to user PATH'));
|
|
69
|
+
} else {
|
|
70
|
+
console.log(chalk.gray(' Already in PATH'));
|
|
71
|
+
}
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.log(chalk.yellow(' Could not update PATH automatically.'));
|
|
74
|
+
console.log(chalk.yellow(` Please add this to your PATH manually: ${installDir}`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
printSuccess('Windows');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function installUnix(currentPath, platform) {
|
|
81
|
+
const installDir = '/usr/local/bin';
|
|
82
|
+
const targetPath = path.join(installDir, 'arm');
|
|
83
|
+
const needsSudo = !isWritable(installDir);
|
|
84
|
+
|
|
85
|
+
console.log(chalk.yellow('[1/2] Copying binary...'));
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
if (needsSudo) {
|
|
89
|
+
console.log(chalk.gray(' (requires sudo)'));
|
|
90
|
+
execSync(`sudo cp "${currentPath}" "${targetPath}"`, { stdio: 'inherit' });
|
|
91
|
+
execSync(`sudo chmod +x "${targetPath}"`, { stdio: 'inherit' });
|
|
92
|
+
} else {
|
|
93
|
+
fs.copyFileSync(currentPath, targetPath);
|
|
94
|
+
fs.chmodSync(targetPath, '755');
|
|
95
|
+
}
|
|
96
|
+
console.log(chalk.green(' Installed to ' + targetPath));
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new Error(`Failed to copy binary: ${err.message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log(chalk.yellow('[2/2] Verifying installation...'));
|
|
102
|
+
try {
|
|
103
|
+
const version = execSync(`"${targetPath}" --version`, { encoding: 'utf8' }).trim();
|
|
104
|
+
console.log(chalk.green(` Installed: arm v${version}`));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.log(chalk.yellow(' Could not verify installation'));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
printSuccess(platform === 'darwin' ? 'macOS' : 'Linux');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isWritable(dir) {
|
|
113
|
+
try {
|
|
114
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
115
|
+
return true;
|
|
116
|
+
} catch {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function printSuccess(platform) {
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(chalk.green(' ✓ Installation complete!'));
|
|
124
|
+
console.log('');
|
|
125
|
+
console.log(chalk.cyan(' To get started:'));
|
|
126
|
+
if (platform === 'Windows') {
|
|
127
|
+
console.log(chalk.white(' 1. Open a NEW terminal window'));
|
|
128
|
+
}
|
|
129
|
+
console.log(chalk.white(' 1. Run: arm login'));
|
|
130
|
+
console.log(chalk.white(' 2. Run: arm tunnel 3000'));
|
|
131
|
+
console.log('');
|
|
132
|
+
console.log(chalk.gray(' Documentation: https://docs.tunnelapi.in'));
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = install;
|
package/commands/login.js
CHANGED
|
@@ -6,6 +6,31 @@ const api = require('../utils/api');
|
|
|
6
6
|
const config = require('../utils/config');
|
|
7
7
|
|
|
8
8
|
async function login(options) {
|
|
9
|
+
// Token-based login for CI/CD automation
|
|
10
|
+
if (options.token) {
|
|
11
|
+
const spinner = ora('Authenticating with token...').start();
|
|
12
|
+
try {
|
|
13
|
+
// Validate token by making a test API call
|
|
14
|
+
config.set('token', options.token);
|
|
15
|
+
const response = await api.getUser();
|
|
16
|
+
|
|
17
|
+
if (response && response.user) {
|
|
18
|
+
config.set('userId', response.user._id || response.user.id);
|
|
19
|
+
config.set('email', response.user.email);
|
|
20
|
+
spinner.succeed(chalk.green('Authenticated successfully!'));
|
|
21
|
+
console.log(chalk.gray(`\nLogged in as: ${response.user.email}\n`));
|
|
22
|
+
} else {
|
|
23
|
+
throw new Error('Invalid token');
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
config.delete('token');
|
|
27
|
+
spinner.fail(chalk.red('Authentication failed'));
|
|
28
|
+
console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
9
34
|
console.log(chalk.blue.bold('\n🔐 API Response Manager - Login\n'));
|
|
10
35
|
|
|
11
36
|
// Check if social login is requested
|
package/commands/tunnel.js
CHANGED
|
@@ -3,19 +3,29 @@ const ora = require('ora');
|
|
|
3
3
|
const WebSocket = require('ws');
|
|
4
4
|
const Table = require('cli-table3');
|
|
5
5
|
const axios = require('axios');
|
|
6
|
+
const net = require('net');
|
|
6
7
|
const api = require('../utils/api');
|
|
7
8
|
const config = require('../utils/config');
|
|
8
9
|
|
|
10
|
+
// Track TCP connections for SSH/TCP tunnels
|
|
11
|
+
const tcpConnections = new Map();
|
|
12
|
+
|
|
9
13
|
// Start tunnel
|
|
10
14
|
async function start(port, options) {
|
|
11
|
-
|
|
15
|
+
const jsonOutput = options.json || false;
|
|
16
|
+
|
|
17
|
+
if (!jsonOutput) {
|
|
18
|
+
console.log(chalk.blue.bold('\n🚇 Starting Tunnel...\n'));
|
|
19
|
+
}
|
|
12
20
|
|
|
13
21
|
if (!port) {
|
|
14
22
|
port = config.get('defaultTunnelPort');
|
|
15
|
-
|
|
23
|
+
if (!jsonOutput) {
|
|
24
|
+
console.log(chalk.gray(`Using default port: ${port}`));
|
|
25
|
+
}
|
|
16
26
|
}
|
|
17
27
|
|
|
18
|
-
const spinner = ora('Creating tunnel...').start();
|
|
28
|
+
const spinner = jsonOutput ? null : ora('Creating tunnel...').start();
|
|
19
29
|
|
|
20
30
|
try {
|
|
21
31
|
const tunnelData = {
|
|
@@ -37,7 +47,22 @@ async function start(port, options) {
|
|
|
37
47
|
const response = await api.createTunnel(tunnelData);
|
|
38
48
|
const tunnel = response.tunnel;
|
|
39
49
|
|
|
40
|
-
spinner.succeed(chalk.green('Tunnel created successfully!'));
|
|
50
|
+
if (spinner) spinner.succeed(chalk.green('Tunnel created successfully!'));
|
|
51
|
+
|
|
52
|
+
// JSON output for automation/CI
|
|
53
|
+
if (jsonOutput) {
|
|
54
|
+
console.log(JSON.stringify({
|
|
55
|
+
success: true,
|
|
56
|
+
id: tunnel.id || tunnel._id,
|
|
57
|
+
subdomain: tunnel.subdomain,
|
|
58
|
+
publicUrl: tunnel.publicUrl,
|
|
59
|
+
localPort: tunnel.localPort,
|
|
60
|
+
protocol: tunnel.protocol || 'https'
|
|
61
|
+
}));
|
|
62
|
+
// For JSON mode, connect silently
|
|
63
|
+
await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort, true);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
41
66
|
|
|
42
67
|
// Safe string padding function
|
|
43
68
|
const safePadEnd = (str, length) => {
|
|
@@ -56,17 +81,30 @@ async function start(port, options) {
|
|
|
56
81
|
|
|
57
82
|
// Connect tunnel client
|
|
58
83
|
console.log(chalk.blue('Connecting tunnel client...\n'));
|
|
59
|
-
await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort);
|
|
84
|
+
await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort, options.protocol || 'http', false);
|
|
60
85
|
|
|
61
86
|
} catch (error) {
|
|
62
|
-
spinner.fail(chalk.red('Failed to create tunnel'));
|
|
63
|
-
|
|
87
|
+
if (spinner) spinner.fail(chalk.red('Failed to create tunnel'));
|
|
88
|
+
|
|
89
|
+
const errorMsg = error.response?.data?.msg || error.message;
|
|
90
|
+
|
|
91
|
+
// Handle subdomain already taken error
|
|
92
|
+
if (error.response?.status === 409 && errorMsg.toLowerCase().includes('subdomain')) {
|
|
93
|
+
console.error(chalk.yellow(`\n⚠ ${errorMsg}`));
|
|
94
|
+
console.log(chalk.gray('\nOptions:'));
|
|
95
|
+
console.log(chalk.gray(' 1. Try again with a different subdomain: ') + chalk.cyan(`arm tunnel ${port} -s <new-subdomain>`));
|
|
96
|
+
console.log(chalk.gray(' 2. Let the system generate a unique subdomain: ') + chalk.cyan(`arm tunnel ${port}`));
|
|
97
|
+
console.log();
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.error(chalk.red(`\n✗ ${errorMsg}\n`));
|
|
64
102
|
process.exit(1);
|
|
65
103
|
}
|
|
66
104
|
}
|
|
67
105
|
|
|
68
106
|
// Connect tunnel client
|
|
69
|
-
async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
107
|
+
async function connectTunnelClient(tunnelId, subdomain, localPort, protocol = 'http', silent = false) {
|
|
70
108
|
const tunnelServerUrl = config.get('tunnelServerUrl') || 'ws://localhost:8080';
|
|
71
109
|
const token = api.getToken();
|
|
72
110
|
const userId = config.get('userId');
|
|
@@ -74,15 +112,17 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
|
74
112
|
const ws = new WebSocket(tunnelServerUrl);
|
|
75
113
|
|
|
76
114
|
let heartbeatInterval;
|
|
115
|
+
const isTcpTunnel = protocol === 'tcp' || protocol === 'ssh';
|
|
77
116
|
|
|
78
117
|
ws.on('open', () => {
|
|
79
|
-
console.log(chalk.green('✓ Connected to tunnel server'));
|
|
118
|
+
if (!silent) console.log(chalk.green('✓ Connected to tunnel server'));
|
|
80
119
|
|
|
81
120
|
ws.send(JSON.stringify({
|
|
82
121
|
type: 'register',
|
|
83
122
|
tunnelId,
|
|
84
123
|
subdomain,
|
|
85
124
|
localPort,
|
|
125
|
+
protocol,
|
|
86
126
|
authToken: token,
|
|
87
127
|
userId
|
|
88
128
|
}));
|
|
@@ -99,13 +139,42 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
|
99
139
|
const message = JSON.parse(data.toString());
|
|
100
140
|
|
|
101
141
|
if (message.type === 'registered') {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
142
|
+
if (!silent) {
|
|
143
|
+
console.log(chalk.green.bold('\n🎉 Tunnel Active!\n'));
|
|
144
|
+
console.log(chalk.white('Your local server is now accessible at:'));
|
|
145
|
+
console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
|
|
146
|
+
|
|
147
|
+
// Show TCP/SSH specific info
|
|
148
|
+
if (isTcpTunnel && message.tcpHost && message.tcpPort) {
|
|
149
|
+
console.log(chalk.yellow.bold('🔌 TCP/SSH Connection Info:\n'));
|
|
150
|
+
console.log(chalk.white(` TCP Host: ${message.tcpHost}`));
|
|
151
|
+
console.log(chalk.white(` TCP Port: ${message.tcpPort}\n`));
|
|
152
|
+
if (message.sshCommand) {
|
|
153
|
+
console.log(chalk.gray('SSH Command (Linux/Mac):'));
|
|
154
|
+
console.log(chalk.cyan(` ${message.sshCommand}\n`));
|
|
155
|
+
console.log(chalk.gray('SSH Command (Windows with ncat):'));
|
|
156
|
+
console.log(chalk.cyan(` ssh -o ProxyCommand="ncat ${message.tcpHost} ${message.tcpPort}" user@${message.tcpHost}\n`));
|
|
157
|
+
console.log(chalk.gray('SSH with Key (passwordless):'));
|
|
158
|
+
console.log(chalk.cyan(` ssh -i ~/.ssh/your_key.pem -o ProxyCommand="echo 'SUBDOMAIN:${subdomain}' | nc %h %p" user@${message.tcpHost} -p ${message.tcpPort}\n`));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
console.log(chalk.gray('Press Ctrl+C to stop the tunnel\n'));
|
|
163
|
+
}
|
|
164
|
+
} else if (message.type === 'tcp-connect') {
|
|
165
|
+
// Handle new TCP connection for SSH/TCP tunnels
|
|
166
|
+
handleTcpConnect(message, localPort, ws, silent);
|
|
167
|
+
} else if (message.type === 'tcp-data') {
|
|
168
|
+
// Handle TCP data from tunnel server
|
|
169
|
+
handleTcpData(message, ws, silent);
|
|
170
|
+
} else if (message.type === 'tcp-end') {
|
|
171
|
+
// Handle TCP connection close
|
|
172
|
+
handleTcpEnd(message, silent);
|
|
106
173
|
} else if (message.type === 'request') {
|
|
107
|
-
|
|
108
|
-
|
|
174
|
+
if (!silent) {
|
|
175
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
176
|
+
console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
|
|
177
|
+
}
|
|
109
178
|
|
|
110
179
|
// Forward request to local server
|
|
111
180
|
try {
|
|
@@ -117,15 +186,34 @@ async function connectTunnelClient(tunnelId, subdomain, localPort) {
|
|
|
117
186
|
delete forwardHeaders['connection'];
|
|
118
187
|
delete forwardHeaders['accept-encoding']; // Prevent compression issues
|
|
119
188
|
|
|
189
|
+
// Handle body - check if it's base64 encoded (binary/multipart data)
|
|
190
|
+
let requestBody = message.body;
|
|
191
|
+
if (message.body && typeof message.body === 'object' && message.body._isBase64) {
|
|
192
|
+
// Decode base64 body back to Buffer
|
|
193
|
+
requestBody = Buffer.from(message.body.data, 'base64');
|
|
194
|
+
if (!silent) {
|
|
195
|
+
console.log(chalk.gray(` 📎 Decoded ${message.body._isMultipart ? 'multipart' : 'binary'} body: ${requestBody.length} bytes`));
|
|
196
|
+
}
|
|
197
|
+
// For multipart, keep the original content-length
|
|
198
|
+
if (!message.body._isMultipart) {
|
|
199
|
+
delete forwardHeaders['content-length'];
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
// For non-binary data, let axios calculate content-length
|
|
203
|
+
delete forwardHeaders['content-length'];
|
|
204
|
+
}
|
|
205
|
+
|
|
120
206
|
const response = await axios({
|
|
121
207
|
method: message.method.toLowerCase(),
|
|
122
208
|
url: localUrl,
|
|
123
209
|
headers: forwardHeaders,
|
|
124
|
-
data:
|
|
210
|
+
data: requestBody,
|
|
125
211
|
validateStatus: () => true, // Accept any status code
|
|
126
212
|
responseType: 'arraybuffer', // Handle binary data properly
|
|
127
213
|
maxRedirects: 0, // Don't follow redirects, let the client handle them
|
|
128
|
-
timeout: 25000 // 25 second timeout
|
|
214
|
+
timeout: 25000, // 25 second timeout
|
|
215
|
+
maxBodyLength: Infinity, // Allow large file uploads
|
|
216
|
+
maxContentLength: Infinity // Allow large responses
|
|
129
217
|
});
|
|
130
218
|
|
|
131
219
|
// Clean up headers to avoid conflicts
|
|
@@ -259,6 +347,38 @@ async function stop(tunnelId) {
|
|
|
259
347
|
}
|
|
260
348
|
}
|
|
261
349
|
|
|
350
|
+
// Stop all tunnels
|
|
351
|
+
async function stopAll() {
|
|
352
|
+
const spinner = ora('Stopping all tunnels...').start();
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
const response = await api.getTunnels();
|
|
356
|
+
const tunnels = response.tunnels || [];
|
|
357
|
+
|
|
358
|
+
if (tunnels.length === 0) {
|
|
359
|
+
spinner.info(chalk.yellow('No active tunnels to stop'));
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let stopped = 0;
|
|
364
|
+
for (const tunnel of tunnels) {
|
|
365
|
+
try {
|
|
366
|
+
await api.deleteTunnel(tunnel._id || tunnel.id);
|
|
367
|
+
stopped++;
|
|
368
|
+
} catch (e) {
|
|
369
|
+
// Continue stopping other tunnels
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
spinner.succeed(chalk.green(`Stopped ${stopped} tunnel(s)`));
|
|
374
|
+
console.log();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
spinner.fail(chalk.red('Failed to stop tunnels'));
|
|
377
|
+
console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
262
382
|
// View tunnel logs
|
|
263
383
|
async function logs(tunnelId, options) {
|
|
264
384
|
const spinner = ora('Fetching logs...').start();
|
|
@@ -433,10 +553,106 @@ async function configureIngress(tunnelId, rules, options) {
|
|
|
433
553
|
}
|
|
434
554
|
}
|
|
435
555
|
|
|
556
|
+
// TCP connection handlers for SSH/TCP tunnels
|
|
557
|
+
function handleTcpConnect(message, localPort, ws, silent) {
|
|
558
|
+
const { connectionId, remoteAddress, remotePort } = message;
|
|
559
|
+
|
|
560
|
+
if (!silent) {
|
|
561
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
562
|
+
console.log(chalk.gray(`[${timestamp}]`), chalk.magenta('TCP'), chalk.white(`Connection from ${remoteAddress}:${remotePort}`));
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Create connection to local server (e.g., SSH on port 22)
|
|
566
|
+
const localSocket = net.createConnection({
|
|
567
|
+
host: 'localhost',
|
|
568
|
+
port: localPort
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
tcpConnections.set(connectionId, localSocket);
|
|
572
|
+
|
|
573
|
+
localSocket.on('connect', () => {
|
|
574
|
+
if (!silent) {
|
|
575
|
+
console.log(chalk.green(` ✓ Connected to local port ${localPort}`));
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
localSocket.on('data', (data) => {
|
|
580
|
+
// Forward data back to tunnel server
|
|
581
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
582
|
+
ws.send(JSON.stringify({
|
|
583
|
+
type: 'tcp-response',
|
|
584
|
+
connectionId,
|
|
585
|
+
data: data.toString('base64')
|
|
586
|
+
}));
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
localSocket.on('end', () => {
|
|
591
|
+
if (!silent) {
|
|
592
|
+
console.log(chalk.gray(` TCP connection ended: ${connectionId.substring(0, 8)}...`));
|
|
593
|
+
}
|
|
594
|
+
tcpConnections.delete(connectionId);
|
|
595
|
+
|
|
596
|
+
// Notify tunnel server
|
|
597
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
598
|
+
ws.send(JSON.stringify({
|
|
599
|
+
type: 'tcp-close',
|
|
600
|
+
connectionId
|
|
601
|
+
}));
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
localSocket.on('error', (error) => {
|
|
606
|
+
console.error(chalk.red(` TCP error: ${error.message}`));
|
|
607
|
+
tcpConnections.delete(connectionId);
|
|
608
|
+
|
|
609
|
+
// Notify tunnel server
|
|
610
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
611
|
+
ws.send(JSON.stringify({
|
|
612
|
+
type: 'tcp-close',
|
|
613
|
+
connectionId
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function handleTcpData(message, ws, silent) {
|
|
620
|
+
const { connectionId, data } = message;
|
|
621
|
+
|
|
622
|
+
const localSocket = tcpConnections.get(connectionId);
|
|
623
|
+
if (!localSocket) {
|
|
624
|
+
if (!silent) {
|
|
625
|
+
console.warn(chalk.yellow(` No local socket found for ${connectionId.substring(0, 8)}...`));
|
|
626
|
+
}
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const buffer = Buffer.from(data, 'base64');
|
|
632
|
+
localSocket.write(buffer);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
console.error(chalk.red(` Error writing to local socket: ${error.message}`));
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function handleTcpEnd(message, silent) {
|
|
639
|
+
const { connectionId } = message;
|
|
640
|
+
|
|
641
|
+
const localSocket = tcpConnections.get(connectionId);
|
|
642
|
+
if (localSocket) {
|
|
643
|
+
localSocket.end();
|
|
644
|
+
tcpConnections.delete(connectionId);
|
|
645
|
+
if (!silent) {
|
|
646
|
+
console.log(chalk.gray(` TCP connection closed: ${connectionId.substring(0, 8)}...`));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
436
651
|
module.exports = {
|
|
437
652
|
start,
|
|
438
653
|
list,
|
|
439
654
|
stop,
|
|
655
|
+
stopAll,
|
|
440
656
|
logs,
|
|
441
657
|
setDomain,
|
|
442
658
|
uploadSSL,
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
|
|
7
|
+
const uninstall = async () => {
|
|
8
|
+
const platform = os.platform();
|
|
9
|
+
|
|
10
|
+
console.log('');
|
|
11
|
+
console.log(chalk.cyan(' TunnelAPI CLI Uninstaller'));
|
|
12
|
+
console.log(chalk.cyan(' ========================='));
|
|
13
|
+
console.log('');
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
if (platform === 'win32') {
|
|
17
|
+
await uninstallWindows();
|
|
18
|
+
} else if (platform === 'darwin' || platform === 'linux') {
|
|
19
|
+
await uninstallUnix(platform);
|
|
20
|
+
} else {
|
|
21
|
+
console.log(chalk.red(` Unsupported platform: ${platform}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.log(chalk.red(` Uninstall failed: ${error.message}`));
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
async function uninstallWindows() {
|
|
31
|
+
const installDir = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'TunnelAPI');
|
|
32
|
+
const binaryPath = path.join(installDir, 'arm.exe');
|
|
33
|
+
|
|
34
|
+
console.log(chalk.yellow('[1/3] Removing from PATH...'));
|
|
35
|
+
try {
|
|
36
|
+
const currentUserPath = execSync('powershell -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"', { encoding: 'utf8' }).trim();
|
|
37
|
+
|
|
38
|
+
if (currentUserPath.includes(installDir)) {
|
|
39
|
+
// Remove install dir from PATH
|
|
40
|
+
const pathParts = currentUserPath.split(';').filter(p => p && !p.includes('TunnelAPI'));
|
|
41
|
+
const newPath = pathParts.join(';');
|
|
42
|
+
execSync(`powershell -Command "[Environment]::SetEnvironmentVariable('Path', '${newPath.replace(/'/g, "''")}', 'User')"`, { encoding: 'utf8' });
|
|
43
|
+
console.log(chalk.green(' Removed from user PATH'));
|
|
44
|
+
} else {
|
|
45
|
+
console.log(chalk.gray(' Not in PATH'));
|
|
46
|
+
}
|
|
47
|
+
} catch (err) {
|
|
48
|
+
console.log(chalk.yellow(' Could not update PATH automatically'));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log(chalk.yellow('[2/3] Removing binary...'));
|
|
52
|
+
if (fs.existsSync(binaryPath)) {
|
|
53
|
+
try {
|
|
54
|
+
fs.unlinkSync(binaryPath);
|
|
55
|
+
console.log(chalk.green(' Removed ' + binaryPath));
|
|
56
|
+
} catch (err) {
|
|
57
|
+
console.log(chalk.yellow(' Could not remove binary (may be in use)'));
|
|
58
|
+
console.log(chalk.yellow(` Please manually delete: ${binaryPath}`));
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
console.log(chalk.gray(' Binary not found at ' + binaryPath));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(chalk.yellow('[3/3] Cleaning up install directory...'));
|
|
65
|
+
if (fs.existsSync(installDir)) {
|
|
66
|
+
try {
|
|
67
|
+
const files = fs.readdirSync(installDir);
|
|
68
|
+
if (files.length === 0) {
|
|
69
|
+
fs.rmdirSync(installDir);
|
|
70
|
+
console.log(chalk.green(' Removed install directory'));
|
|
71
|
+
} else {
|
|
72
|
+
console.log(chalk.gray(' Directory not empty, keeping it'));
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.log(chalk.gray(' Could not remove directory'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
printSuccess('Windows');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function uninstallUnix(platform) {
|
|
83
|
+
const binaryPath = '/usr/local/bin/arm';
|
|
84
|
+
const needsSudo = !isWritable('/usr/local/bin');
|
|
85
|
+
|
|
86
|
+
console.log(chalk.yellow('[1/2] Checking for binary...'));
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(binaryPath)) {
|
|
89
|
+
console.log(chalk.gray(' Binary not found at ' + binaryPath));
|
|
90
|
+
console.log(chalk.yellow(' Checking other locations...'));
|
|
91
|
+
|
|
92
|
+
// Check common locations
|
|
93
|
+
const altPaths = [
|
|
94
|
+
path.join(os.homedir(), '.local', 'bin', 'arm'),
|
|
95
|
+
'/usr/bin/arm',
|
|
96
|
+
'/opt/tunnelapi/arm'
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
let found = false;
|
|
100
|
+
for (const altPath of altPaths) {
|
|
101
|
+
if (fs.existsSync(altPath)) {
|
|
102
|
+
console.log(chalk.gray(` Found at ${altPath}`));
|
|
103
|
+
found = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!found) {
|
|
108
|
+
console.log(chalk.gray(' No installation found'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
printSuccess(platform === 'darwin' ? 'macOS' : 'Linux');
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(chalk.yellow('[2/2] Removing binary...'));
|
|
116
|
+
try {
|
|
117
|
+
if (needsSudo) {
|
|
118
|
+
console.log(chalk.gray(' (requires sudo)'));
|
|
119
|
+
execSync(`sudo rm -f "${binaryPath}"`, { stdio: 'inherit' });
|
|
120
|
+
} else {
|
|
121
|
+
fs.unlinkSync(binaryPath);
|
|
122
|
+
}
|
|
123
|
+
console.log(chalk.green(' Removed ' + binaryPath));
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.log(chalk.red(` Failed to remove: ${err.message}`));
|
|
126
|
+
console.log(chalk.yellow(` Try manually: sudo rm ${binaryPath}`));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
printSuccess(platform === 'darwin' ? 'macOS' : 'Linux');
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isWritable(dir) {
|
|
133
|
+
try {
|
|
134
|
+
fs.accessSync(dir, fs.constants.W_OK);
|
|
135
|
+
return true;
|
|
136
|
+
} catch {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function printSuccess(platform) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.green(' ✓ Uninstall complete!'));
|
|
144
|
+
console.log('');
|
|
145
|
+
if (platform === 'Windows') {
|
|
146
|
+
console.log(chalk.cyan(' Note: Open a NEW terminal for PATH changes to take effect.'));
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk.gray(' Thank you for using TunnelAPI!'));
|
|
149
|
+
console.log(chalk.gray(' Feedback: info@tunnelapi.in'));
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = uninstall;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-response-manager",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.4",
|
|
4
4
|
"description": "Command-line interface for API Response Manager",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,30 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"test": "jest",
|
|
11
11
|
"link": "npm link",
|
|
12
|
-
"prepublishOnly": "echo 'Publishing api-response-manager CLI...'"
|
|
12
|
+
"prepublishOnly": "echo 'Publishing api-response-manager CLI...'",
|
|
13
|
+
"build": "pkg . --out-path dist",
|
|
14
|
+
"build:win": "pkg . -t node18-win-x64 --out-path dist",
|
|
15
|
+
"build:mac": "pkg . -t node18-macos-x64 --out-path dist",
|
|
16
|
+
"build:mac-arm": "pkg . -t node18-macos-arm64 --out-path dist",
|
|
17
|
+
"build:linux": "pkg . -t node18-linux-x64 --out-path dist",
|
|
18
|
+
"build:all": "pkg . -t node18-win-x64,node18-macos-x64,node18-macos-arm64,node18-linux-x64 --out-path dist"
|
|
19
|
+
},
|
|
20
|
+
"pkg": {
|
|
21
|
+
"scripts": [
|
|
22
|
+
"bin/**/*.js",
|
|
23
|
+
"commands/**/*.js",
|
|
24
|
+
"utils/**/*.js"
|
|
25
|
+
],
|
|
26
|
+
"assets": [
|
|
27
|
+
"node_modules/**/*"
|
|
28
|
+
],
|
|
29
|
+
"targets": [
|
|
30
|
+
"node18-win-x64",
|
|
31
|
+
"node18-macos-x64",
|
|
32
|
+
"node18-macos-arm64",
|
|
33
|
+
"node18-linux-x64"
|
|
34
|
+
],
|
|
35
|
+
"outputPath": "dist"
|
|
13
36
|
},
|
|
14
37
|
"files": [
|
|
15
38
|
"bin/",
|
|
@@ -45,7 +68,9 @@
|
|
|
45
68
|
"ws": "^8.14.2"
|
|
46
69
|
},
|
|
47
70
|
"devDependencies": {
|
|
48
|
-
"
|
|
71
|
+
"@yao-pkg/pkg": "^5.11.0",
|
|
72
|
+
"jest": "^29.7.0",
|
|
73
|
+
"rcedit": "^5.0.2"
|
|
49
74
|
},
|
|
50
75
|
"engines": {
|
|
51
76
|
"node": ">=14.0.0"
|
package/utils/api.js
CHANGED
|
@@ -59,6 +59,11 @@ class APIClient {
|
|
|
59
59
|
return response.data;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
async getUser() {
|
|
63
|
+
const response = await this.client.get('/auth/me');
|
|
64
|
+
return response.data;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
// OAuth Device Flow for CLI
|
|
63
68
|
async requestDeviceCode(provider) {
|
|
64
69
|
const response = await this.client.post('/auth/device/code', { provider });
|