api-response-manager 2.5.3 → 2.6.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.
- package/README.md +1 -1
- package/bin/arm.js +26 -1
- package/commands/install.js +136 -0
- package/commands/serve.js +569 -0
- package/commands/tunnel.js +161 -4
- package/commands/uninstall.js +153 -0
- package/package.json +30 -3
- package/utils/api.js +11 -0
- package/utils/e2eEncryption.js +162 -0
package/README.md
CHANGED
package/bin/arm.js
CHANGED
|
@@ -24,6 +24,9 @@ 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');
|
|
29
|
+
const serveCommand = require('../commands/serve');
|
|
27
30
|
|
|
28
31
|
// CLI setup
|
|
29
32
|
program
|
|
@@ -56,9 +59,10 @@ program
|
|
|
56
59
|
.option('-n, --name <name>', 'Tunnel name')
|
|
57
60
|
.option('-a, --auth', 'Enable basic authentication')
|
|
58
61
|
.option('-r, --rate-limit <limit>', 'Rate limit (requests per minute)', '60')
|
|
59
|
-
.option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ws, wss)', 'http')
|
|
62
|
+
.option('-p, --protocol <protocol>', 'Protocol (http, https, tcp, ssh, ws, wss, udp)', 'http')
|
|
60
63
|
.option('--ssl', 'Enable SSL/HTTPS')
|
|
61
64
|
.option('-d, --domain <domain>', 'Custom domain')
|
|
65
|
+
.option('--e2e', 'Enable end-to-end encryption')
|
|
62
66
|
.option('--json', 'Output in JSON format (for CI/CD automation)')
|
|
63
67
|
.action(tunnelCommand.start);
|
|
64
68
|
|
|
@@ -141,6 +145,15 @@ program
|
|
|
141
145
|
.option('--tls', 'Enable TLS for ingress')
|
|
142
146
|
.action(tunnelCommand.configureIngress);
|
|
143
147
|
|
|
148
|
+
// Serve command - file sharing
|
|
149
|
+
program
|
|
150
|
+
.command('serve')
|
|
151
|
+
.description('Share a directory over the internet')
|
|
152
|
+
.argument('[directory]', 'Directory to serve (default: current directory)', '.')
|
|
153
|
+
.option('-p, --port <port>', 'Local port for file server', '8000')
|
|
154
|
+
.option('-s, --subdomain <subdomain>', 'Custom subdomain')
|
|
155
|
+
.action(serveCommand.serve);
|
|
156
|
+
|
|
144
157
|
// IP Whitelist commands
|
|
145
158
|
program
|
|
146
159
|
.command('tunnel:ip-whitelist:add')
|
|
@@ -429,6 +442,18 @@ program
|
|
|
429
442
|
.argument('<yamlFile>', 'Path to YAML file')
|
|
430
443
|
.action(ingressCommand.validate);
|
|
431
444
|
|
|
445
|
+
// Install command (self-install with PATH configuration)
|
|
446
|
+
program
|
|
447
|
+
.command('install')
|
|
448
|
+
.description('Install CLI to system and configure PATH')
|
|
449
|
+
.action(installCommand);
|
|
450
|
+
|
|
451
|
+
// Uninstall command (remove CLI and PATH configuration)
|
|
452
|
+
program
|
|
453
|
+
.command('uninstall')
|
|
454
|
+
.description('Uninstall CLI from system and remove from PATH')
|
|
455
|
+
.action(uninstallCommand);
|
|
456
|
+
|
|
432
457
|
// Account commands
|
|
433
458
|
program
|
|
434
459
|
.command('account')
|
|
@@ -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;
|
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const ora = require('ora');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const http = require('http');
|
|
6
|
+
const url = require('url');
|
|
7
|
+
const mime = require('mime-types');
|
|
8
|
+
const api = require('../utils/api');
|
|
9
|
+
const config = require('../utils/config');
|
|
10
|
+
|
|
11
|
+
// File server for serving static files
|
|
12
|
+
class FileServer {
|
|
13
|
+
constructor(directory, port) {
|
|
14
|
+
this.directory = path.resolve(directory);
|
|
15
|
+
this.port = port;
|
|
16
|
+
this.server = null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
start() {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
this.server = http.createServer((req, res) => {
|
|
22
|
+
this.handleRequest(req, res);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
this.server.on('error', (err) => {
|
|
26
|
+
if (err.code === 'EADDRINUSE') {
|
|
27
|
+
reject(new Error(`Port ${this.port} is already in use`));
|
|
28
|
+
} else {
|
|
29
|
+
reject(err);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
this.server.listen(this.port, () => {
|
|
34
|
+
resolve();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
handleRequest(req, res) {
|
|
40
|
+
const parsedUrl = url.parse(req.url, true);
|
|
41
|
+
let pathname = decodeURIComponent(parsedUrl.pathname);
|
|
42
|
+
|
|
43
|
+
// Prevent directory traversal
|
|
44
|
+
pathname = pathname.replace(/\.\./g, '');
|
|
45
|
+
|
|
46
|
+
let filePath = path.join(this.directory, pathname);
|
|
47
|
+
|
|
48
|
+
// Check if path exists
|
|
49
|
+
fs.stat(filePath, (err, stats) => {
|
|
50
|
+
if (err) {
|
|
51
|
+
if (err.code === 'ENOENT') {
|
|
52
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
53
|
+
res.end(this.generate404Page(pathname));
|
|
54
|
+
} else {
|
|
55
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
56
|
+
res.end('Internal Server Error');
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (stats.isDirectory()) {
|
|
62
|
+
// Check for index.html
|
|
63
|
+
const indexPath = path.join(filePath, 'index.html');
|
|
64
|
+
if (fs.existsSync(indexPath)) {
|
|
65
|
+
this.serveFile(indexPath, res);
|
|
66
|
+
} else {
|
|
67
|
+
// Generate directory listing
|
|
68
|
+
this.serveDirectoryListing(filePath, pathname, res);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
this.serveFile(filePath, res);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
serveFile(filePath, res) {
|
|
77
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
78
|
+
// Handle JSX/TSX files as JavaScript for development
|
|
79
|
+
let contentType;
|
|
80
|
+
if (ext === '.jsx' || ext === '.tsx') {
|
|
81
|
+
contentType = 'application/javascript';
|
|
82
|
+
} else if (ext === '.ts') {
|
|
83
|
+
contentType = 'application/javascript';
|
|
84
|
+
} else {
|
|
85
|
+
contentType = mime.lookup(ext) || 'application/octet-stream';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
fs.readFile(filePath, (err, data) => {
|
|
89
|
+
if (err) {
|
|
90
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
91
|
+
res.end('Error reading file');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const stats = fs.statSync(filePath);
|
|
96
|
+
res.writeHead(200, {
|
|
97
|
+
'Content-Type': contentType,
|
|
98
|
+
'Content-Length': stats.size,
|
|
99
|
+
'Cache-Control': 'no-cache',
|
|
100
|
+
'Access-Control-Allow-Origin': '*'
|
|
101
|
+
});
|
|
102
|
+
res.end(data);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
serveDirectoryListing(dirPath, urlPath, res) {
|
|
107
|
+
fs.readdir(dirPath, { withFileTypes: true }, (err, entries) => {
|
|
108
|
+
if (err) {
|
|
109
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
110
|
+
res.end('Error reading directory');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const html = this.generateDirectoryHTML(entries, urlPath, dirPath);
|
|
115
|
+
res.writeHead(200, {
|
|
116
|
+
'Content-Type': 'text/html',
|
|
117
|
+
'Cache-Control': 'no-cache'
|
|
118
|
+
});
|
|
119
|
+
res.end(html);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
generateDirectoryHTML(entries, urlPath, dirPath) {
|
|
124
|
+
const normalizedPath = urlPath.endsWith('/') ? urlPath : urlPath + '/';
|
|
125
|
+
|
|
126
|
+
let items = entries.map(entry => {
|
|
127
|
+
const isDir = entry.isDirectory();
|
|
128
|
+
const name = entry.name;
|
|
129
|
+
const href = normalizedPath + encodeURIComponent(name) + (isDir ? '/' : '');
|
|
130
|
+
const icon = isDir ? '📁' : this.getFileIcon(name);
|
|
131
|
+
const stats = fs.statSync(path.join(dirPath, name));
|
|
132
|
+
const size = isDir ? '-' : this.formatSize(stats.size);
|
|
133
|
+
const modified = stats.mtime.toLocaleString();
|
|
134
|
+
|
|
135
|
+
return `
|
|
136
|
+
<tr>
|
|
137
|
+
<td class="icon">${icon}</td>
|
|
138
|
+
<td class="name"><a href="${href}">${name}${isDir ? '/' : ''}</a></td>
|
|
139
|
+
<td class="size">${size}</td>
|
|
140
|
+
<td class="modified">${modified}</td>
|
|
141
|
+
</tr>
|
|
142
|
+
`;
|
|
143
|
+
}).join('');
|
|
144
|
+
|
|
145
|
+
// Add parent directory link if not at root
|
|
146
|
+
if (urlPath !== '/' && urlPath !== '') {
|
|
147
|
+
const parentPath = path.dirname(urlPath) || '/';
|
|
148
|
+
items = `
|
|
149
|
+
<tr>
|
|
150
|
+
<td class="icon">📂</td>
|
|
151
|
+
<td class="name"><a href="${parentPath}">..</a></td>
|
|
152
|
+
<td class="size">-</td>
|
|
153
|
+
<td class="modified">-</td>
|
|
154
|
+
</tr>
|
|
155
|
+
` + items;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return `
|
|
159
|
+
<!DOCTYPE html>
|
|
160
|
+
<html>
|
|
161
|
+
<head>
|
|
162
|
+
<meta charset="UTF-8">
|
|
163
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
164
|
+
<title>Index of ${urlPath || '/'}</title>
|
|
165
|
+
<style>
|
|
166
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
167
|
+
body {
|
|
168
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
169
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
170
|
+
color: #e4e4e7;
|
|
171
|
+
min-height: 100vh;
|
|
172
|
+
padding: 2rem;
|
|
173
|
+
}
|
|
174
|
+
.container {
|
|
175
|
+
max-width: 1000px;
|
|
176
|
+
margin: 0 auto;
|
|
177
|
+
}
|
|
178
|
+
h1 {
|
|
179
|
+
font-size: 1.5rem;
|
|
180
|
+
margin-bottom: 1.5rem;
|
|
181
|
+
color: #a78bfa;
|
|
182
|
+
}
|
|
183
|
+
.path {
|
|
184
|
+
color: #94a3b8;
|
|
185
|
+
font-family: monospace;
|
|
186
|
+
}
|
|
187
|
+
table {
|
|
188
|
+
width: 100%;
|
|
189
|
+
border-collapse: collapse;
|
|
190
|
+
background: rgba(255,255,255,0.05);
|
|
191
|
+
border-radius: 0.5rem;
|
|
192
|
+
overflow: hidden;
|
|
193
|
+
}
|
|
194
|
+
th, td {
|
|
195
|
+
padding: 0.75rem 1rem;
|
|
196
|
+
text-align: left;
|
|
197
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
198
|
+
}
|
|
199
|
+
th {
|
|
200
|
+
background: rgba(255,255,255,0.1);
|
|
201
|
+
font-weight: 600;
|
|
202
|
+
color: #94a3b8;
|
|
203
|
+
font-size: 0.875rem;
|
|
204
|
+
}
|
|
205
|
+
tr:hover {
|
|
206
|
+
background: rgba(255,255,255,0.05);
|
|
207
|
+
}
|
|
208
|
+
a {
|
|
209
|
+
color: #60a5fa;
|
|
210
|
+
text-decoration: none;
|
|
211
|
+
}
|
|
212
|
+
a:hover {
|
|
213
|
+
text-decoration: underline;
|
|
214
|
+
}
|
|
215
|
+
.icon { width: 40px; text-align: center; }
|
|
216
|
+
.size { width: 100px; color: #94a3b8; font-size: 0.875rem; }
|
|
217
|
+
.modified { color: #94a3b8; font-size: 0.875rem; }
|
|
218
|
+
.footer {
|
|
219
|
+
margin-top: 2rem;
|
|
220
|
+
text-align: center;
|
|
221
|
+
color: #64748b;
|
|
222
|
+
font-size: 0.875rem;
|
|
223
|
+
}
|
|
224
|
+
.footer a { color: #a78bfa; }
|
|
225
|
+
</style>
|
|
226
|
+
</head>
|
|
227
|
+
<body>
|
|
228
|
+
<div class="container">
|
|
229
|
+
<h1>📂 Index of <span class="path">${urlPath || '/'}</span></h1>
|
|
230
|
+
<table>
|
|
231
|
+
<thead>
|
|
232
|
+
<tr>
|
|
233
|
+
<th></th>
|
|
234
|
+
<th>Name</th>
|
|
235
|
+
<th>Size</th>
|
|
236
|
+
<th>Modified</th>
|
|
237
|
+
</tr>
|
|
238
|
+
</thead>
|
|
239
|
+
<tbody>
|
|
240
|
+
${items}
|
|
241
|
+
</tbody>
|
|
242
|
+
</table>
|
|
243
|
+
<div class="footer">
|
|
244
|
+
Served by <a href="https://tunnelapi.in">TunnelAPI</a> File Server
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</body>
|
|
248
|
+
</html>
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
generate404Page(pathname) {
|
|
253
|
+
return `
|
|
254
|
+
<!DOCTYPE html>
|
|
255
|
+
<html>
|
|
256
|
+
<head>
|
|
257
|
+
<meta charset="UTF-8">
|
|
258
|
+
<title>404 Not Found</title>
|
|
259
|
+
<style>
|
|
260
|
+
body {
|
|
261
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
262
|
+
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
263
|
+
color: #e4e4e7;
|
|
264
|
+
min-height: 100vh;
|
|
265
|
+
display: flex;
|
|
266
|
+
align-items: center;
|
|
267
|
+
justify-content: center;
|
|
268
|
+
text-align: center;
|
|
269
|
+
}
|
|
270
|
+
h1 { font-size: 4rem; color: #ef4444; }
|
|
271
|
+
p { color: #94a3b8; margin-top: 1rem; }
|
|
272
|
+
a { color: #60a5fa; }
|
|
273
|
+
</style>
|
|
274
|
+
</head>
|
|
275
|
+
<body>
|
|
276
|
+
<div>
|
|
277
|
+
<h1>404</h1>
|
|
278
|
+
<p>File not found: ${pathname}</p>
|
|
279
|
+
<p><a href="/">← Back to root</a></p>
|
|
280
|
+
</div>
|
|
281
|
+
</body>
|
|
282
|
+
</html>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getFileIcon(filename) {
|
|
287
|
+
const ext = path.extname(filename).toLowerCase();
|
|
288
|
+
const icons = {
|
|
289
|
+
'.html': '🌐', '.htm': '🌐',
|
|
290
|
+
'.css': '🎨',
|
|
291
|
+
'.js': '📜', '.ts': '📜', '.jsx': '📜', '.tsx': '📜',
|
|
292
|
+
'.json': '📋',
|
|
293
|
+
'.md': '📝', '.txt': '📝',
|
|
294
|
+
'.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️', '.svg': '🖼️', '.webp': '🖼️',
|
|
295
|
+
'.pdf': '📕',
|
|
296
|
+
'.zip': '📦', '.tar': '📦', '.gz': '📦', '.rar': '📦',
|
|
297
|
+
'.mp3': '🎵', '.wav': '🎵', '.ogg': '🎵',
|
|
298
|
+
'.mp4': '🎬', '.webm': '🎬', '.avi': '🎬', '.mov': '🎬',
|
|
299
|
+
'.py': '🐍',
|
|
300
|
+
'.go': '🐹',
|
|
301
|
+
'.rs': '🦀',
|
|
302
|
+
'.java': '☕',
|
|
303
|
+
'.rb': '💎',
|
|
304
|
+
'.php': '🐘',
|
|
305
|
+
'.sh': '🖥️', '.bash': '🖥️',
|
|
306
|
+
'.yml': '⚙️', '.yaml': '⚙️',
|
|
307
|
+
'.xml': '📰',
|
|
308
|
+
'.sql': '🗃️',
|
|
309
|
+
'.env': '🔐',
|
|
310
|
+
};
|
|
311
|
+
return icons[ext] || '📄';
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
formatSize(bytes) {
|
|
315
|
+
if (bytes === 0) return '0 B';
|
|
316
|
+
const k = 1024;
|
|
317
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
318
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
319
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
stop() {
|
|
323
|
+
return new Promise((resolve) => {
|
|
324
|
+
if (this.server) {
|
|
325
|
+
this.server.close(() => resolve());
|
|
326
|
+
} else {
|
|
327
|
+
resolve();
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Serve command - share a directory over the internet
|
|
334
|
+
async function serve(directory, options) {
|
|
335
|
+
const dir = directory || '.';
|
|
336
|
+
const port = parseInt(options.port) || 8000;
|
|
337
|
+
const subdomain = options.subdomain;
|
|
338
|
+
|
|
339
|
+
// Validate directory exists
|
|
340
|
+
const resolvedDir = path.resolve(dir);
|
|
341
|
+
if (!fs.existsSync(resolvedDir)) {
|
|
342
|
+
console.error(chalk.red(`\n✗ Directory not found: ${resolvedDir}\n`));
|
|
343
|
+
process.exit(1);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!fs.statSync(resolvedDir).isDirectory()) {
|
|
347
|
+
console.error(chalk.red(`\n✗ Not a directory: ${resolvedDir}\n`));
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
console.log(chalk.blue.bold('\n📂 Starting File Server...\n'));
|
|
352
|
+
|
|
353
|
+
const spinner = ora('Checking file transfer limits...').start();
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
// Check file transfer limit before starting
|
|
357
|
+
const limitCheck = await api.checkFileTransferLimit();
|
|
358
|
+
if (!limitCheck.allowed) {
|
|
359
|
+
spinner.fail(chalk.red('File transfer limit reached'));
|
|
360
|
+
console.error(chalk.red(`\n✗ ${limitCheck.msg}\n`));
|
|
361
|
+
console.log(chalk.yellow('💡 Upgrade your plan at https://tunnelapi.in/pricing to get more transfer quota.\n'));
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Show remaining quota
|
|
366
|
+
if (limitCheck.limitMB) {
|
|
367
|
+
spinner.succeed(chalk.green(`Transfer quota: ${limitCheck.usedMB} MB / ${limitCheck.limitMB} MB used (${limitCheck.remainingMB} MB remaining)`));
|
|
368
|
+
} else {
|
|
369
|
+
spinner.succeed(chalk.green('Transfer quota: Unlimited'));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
spinner.start('Starting local file server...');
|
|
373
|
+
|
|
374
|
+
// Start local file server
|
|
375
|
+
const fileServer = new FileServer(resolvedDir, port);
|
|
376
|
+
await fileServer.start();
|
|
377
|
+
spinner.succeed(chalk.green(`Local file server running on port ${port}`));
|
|
378
|
+
|
|
379
|
+
// Create tunnel
|
|
380
|
+
spinner.start('Creating tunnel...');
|
|
381
|
+
|
|
382
|
+
const tunnelData = {
|
|
383
|
+
name: subdomain || `file-server-${Date.now()}`,
|
|
384
|
+
subdomain: subdomain,
|
|
385
|
+
localPort: port,
|
|
386
|
+
protocol: 'http'
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const response = await api.createTunnel(tunnelData);
|
|
390
|
+
const tunnel = response.tunnel;
|
|
391
|
+
|
|
392
|
+
spinner.succeed(chalk.green('Tunnel created successfully!'));
|
|
393
|
+
|
|
394
|
+
// Display info
|
|
395
|
+
console.log(chalk.gray('\n┌─────────────────────────────────────────────────────────┐'));
|
|
396
|
+
console.log(chalk.gray('│') + chalk.white.bold(' File Server Information ') + chalk.gray('│'));
|
|
397
|
+
console.log(chalk.gray('├─────────────────────────────────────────────────────────┤'));
|
|
398
|
+
console.log(chalk.gray('│') + chalk.gray(' Directory: ') + chalk.white(resolvedDir.substring(0, 40).padEnd(40)) + chalk.gray('│'));
|
|
399
|
+
console.log(chalk.gray('│') + chalk.gray(' Local Port: ') + chalk.white(String(port).padEnd(40)) + chalk.gray('│'));
|
|
400
|
+
console.log(chalk.gray('│') + chalk.gray(' Public URL: ') + chalk.cyan(tunnel.publicUrl.padEnd(40)) + chalk.gray('│'));
|
|
401
|
+
console.log(chalk.gray('└─────────────────────────────────────────────────────────┘\n'));
|
|
402
|
+
|
|
403
|
+
// Display QR code
|
|
404
|
+
const qrcode = require('qrcode-terminal');
|
|
405
|
+
console.log(chalk.yellow.bold('📱 Scan QR Code to access on mobile:\n'));
|
|
406
|
+
qrcode.generate(tunnel.publicUrl, { small: true }, (qr) => {
|
|
407
|
+
console.log(qr);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
console.log(chalk.gray('\nPress Ctrl+C to stop serving files\n'));
|
|
411
|
+
|
|
412
|
+
// Connect tunnel client
|
|
413
|
+
const tunnelCommand = require('./tunnel');
|
|
414
|
+
// We need to connect the tunnel client to forward requests
|
|
415
|
+
await connectFileTunnel(tunnel.id || tunnel._id, tunnel.subdomain, port);
|
|
416
|
+
|
|
417
|
+
// Handle shutdown
|
|
418
|
+
process.on('SIGINT', async () => {
|
|
419
|
+
console.log(chalk.yellow('\n\n🛑 Stopping file server...'));
|
|
420
|
+
await fileServer.stop();
|
|
421
|
+
try {
|
|
422
|
+
await api.deleteTunnel(tunnel.id || tunnel._id);
|
|
423
|
+
} catch (e) {
|
|
424
|
+
// Ignore errors on cleanup
|
|
425
|
+
}
|
|
426
|
+
console.log(chalk.green('File server stopped.\n'));
|
|
427
|
+
process.exit(0);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
} catch (error) {
|
|
431
|
+
spinner.fail(chalk.red('Failed to start file server'));
|
|
432
|
+
console.error(chalk.red(`\n✗ ${error.response?.data?.msg || error.message}\n`));
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Connect tunnel client for file serving
|
|
438
|
+
async function connectFileTunnel(tunnelId, subdomain, localPort) {
|
|
439
|
+
const WebSocket = require('ws');
|
|
440
|
+
const axios = require('axios');
|
|
441
|
+
|
|
442
|
+
const tunnelServerUrl = config.get('tunnelServerUrl') || 'wss://tunnel.tunnelapi.in';
|
|
443
|
+
const token = api.getToken();
|
|
444
|
+
const userId = config.get('userId');
|
|
445
|
+
|
|
446
|
+
const ws = new WebSocket(tunnelServerUrl);
|
|
447
|
+
|
|
448
|
+
ws.on('open', () => {
|
|
449
|
+
console.log(chalk.green('✓ Connected to tunnel server'));
|
|
450
|
+
|
|
451
|
+
ws.send(JSON.stringify({
|
|
452
|
+
type: 'register',
|
|
453
|
+
tunnelId,
|
|
454
|
+
subdomain,
|
|
455
|
+
localPort,
|
|
456
|
+
protocol: 'http',
|
|
457
|
+
authToken: token,
|
|
458
|
+
userId
|
|
459
|
+
}));
|
|
460
|
+
|
|
461
|
+
// Heartbeat
|
|
462
|
+
setInterval(() => {
|
|
463
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
464
|
+
ws.send(JSON.stringify({ type: 'heartbeat', tunnelId, subdomain }));
|
|
465
|
+
}
|
|
466
|
+
}, 30000);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
// Track total bytes transferred
|
|
470
|
+
let totalBytesTransferred = 0;
|
|
471
|
+
let limitReached = false;
|
|
472
|
+
|
|
473
|
+
ws.on('message', async (data) => {
|
|
474
|
+
const message = JSON.parse(data.toString());
|
|
475
|
+
|
|
476
|
+
if (message.type === 'registered') {
|
|
477
|
+
console.log(chalk.green.bold('\n🎉 File Server is now publicly accessible!\n'));
|
|
478
|
+
console.log(chalk.white('Share this URL:'));
|
|
479
|
+
console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
|
|
480
|
+
} else if (message.type === 'request') {
|
|
481
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
482
|
+
|
|
483
|
+
// Check if limit was reached
|
|
484
|
+
if (limitReached) {
|
|
485
|
+
console.log(chalk.red(`[${timestamp}]`), chalk.red('BLOCKED'), chalk.white(message.path), chalk.red('- Transfer limit reached'));
|
|
486
|
+
ws.send(JSON.stringify({
|
|
487
|
+
type: 'response',
|
|
488
|
+
requestId: message.requestId,
|
|
489
|
+
statusCode: 429,
|
|
490
|
+
headers: { 'content-type': 'application/json' },
|
|
491
|
+
body: Buffer.from(JSON.stringify({
|
|
492
|
+
error: 'Monthly file transfer limit reached',
|
|
493
|
+
message: 'Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.'
|
|
494
|
+
})).toString('base64'),
|
|
495
|
+
encoding: 'base64'
|
|
496
|
+
}));
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
console.log(chalk.gray(`[${timestamp}]`), chalk.blue(message.method), chalk.white(message.path));
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const localUrl = `http://localhost:${localPort}${message.path}`;
|
|
504
|
+
|
|
505
|
+
const response = await axios({
|
|
506
|
+
method: message.method.toLowerCase(),
|
|
507
|
+
url: localUrl,
|
|
508
|
+
headers: { ...message.headers, host: `localhost:${localPort}` },
|
|
509
|
+
data: message.body,
|
|
510
|
+
validateStatus: () => true,
|
|
511
|
+
responseType: 'arraybuffer',
|
|
512
|
+
maxRedirects: 0,
|
|
513
|
+
timeout: 30000
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const cleanHeaders = { ...response.headers };
|
|
517
|
+
delete cleanHeaders['transfer-encoding'];
|
|
518
|
+
delete cleanHeaders['connection'];
|
|
519
|
+
|
|
520
|
+
const responseData = Buffer.from(response.data);
|
|
521
|
+
const bodyBase64 = responseData.toString('base64');
|
|
522
|
+
const bytesTransferred = responseData.length;
|
|
523
|
+
|
|
524
|
+
// Track file transfer usage
|
|
525
|
+
try {
|
|
526
|
+
await api.trackFileTransfer(userId, bytesTransferred);
|
|
527
|
+
totalBytesTransferred += bytesTransferred;
|
|
528
|
+
} catch (trackError) {
|
|
529
|
+
if (trackError.response?.data?.limitReached) {
|
|
530
|
+
limitReached = true;
|
|
531
|
+
console.log(chalk.red.bold('\n⚠️ Monthly file transfer limit reached!'));
|
|
532
|
+
console.log(chalk.yellow('💡 Upgrade your plan at https://tunnelapi.in/pricing for more transfer quota.\n'));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
ws.send(JSON.stringify({
|
|
537
|
+
type: 'response',
|
|
538
|
+
requestId: message.requestId,
|
|
539
|
+
statusCode: response.status,
|
|
540
|
+
headers: cleanHeaders,
|
|
541
|
+
body: bodyBase64,
|
|
542
|
+
encoding: 'base64'
|
|
543
|
+
}));
|
|
544
|
+
} catch (error) {
|
|
545
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
546
|
+
ws.send(JSON.stringify({
|
|
547
|
+
type: 'response',
|
|
548
|
+
requestId: message.requestId,
|
|
549
|
+
statusCode: 502,
|
|
550
|
+
headers: { 'content-type': 'application/json' },
|
|
551
|
+
body: Buffer.from(JSON.stringify({ error: 'File server error' })).toString('base64'),
|
|
552
|
+
encoding: 'base64'
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
ws.on('error', (error) => {
|
|
559
|
+
console.error(chalk.red(`WebSocket error: ${error.message}`));
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
ws.on('close', () => {
|
|
563
|
+
console.log(chalk.yellow('Tunnel connection closed'));
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
module.exports = {
|
|
568
|
+
serve
|
|
569
|
+
};
|
package/commands/tunnel.js
CHANGED
|
@@ -3,9 +3,22 @@ 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');
|
|
7
|
+
const qrcode = require('qrcode-terminal');
|
|
6
8
|
const api = require('../utils/api');
|
|
7
9
|
const config = require('../utils/config');
|
|
8
10
|
|
|
11
|
+
// Track TCP connections for SSH/TCP tunnels
|
|
12
|
+
const tcpConnections = new Map();
|
|
13
|
+
|
|
14
|
+
// Generate and display QR code for URL
|
|
15
|
+
function displayQRCode(url, small = true) {
|
|
16
|
+
console.log(chalk.yellow.bold('\n📱 Scan QR Code to access on mobile:\n'));
|
|
17
|
+
qrcode.generate(url, { small }, (qr) => {
|
|
18
|
+
console.log(qr);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
// Start tunnel
|
|
10
23
|
async function start(port, options) {
|
|
11
24
|
const jsonOutput = options.json || false;
|
|
@@ -77,7 +90,7 @@ async function start(port, options) {
|
|
|
77
90
|
|
|
78
91
|
// Connect tunnel client
|
|
79
92
|
console.log(chalk.blue('Connecting tunnel client...\n'));
|
|
80
|
-
await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort, false);
|
|
93
|
+
await connectTunnelClient(tunnel.id || tunnel._id, tunnel.subdomain, tunnel.localPort, options.protocol || 'http', false);
|
|
81
94
|
|
|
82
95
|
} catch (error) {
|
|
83
96
|
if (spinner) spinner.fail(chalk.red('Failed to create tunnel'));
|
|
@@ -100,7 +113,7 @@ async function start(port, options) {
|
|
|
100
113
|
}
|
|
101
114
|
|
|
102
115
|
// Connect tunnel client
|
|
103
|
-
async function connectTunnelClient(tunnelId, subdomain, localPort, silent = false) {
|
|
116
|
+
async function connectTunnelClient(tunnelId, subdomain, localPort, protocol = 'http', silent = false) {
|
|
104
117
|
const tunnelServerUrl = config.get('tunnelServerUrl') || 'ws://localhost:8080';
|
|
105
118
|
const token = api.getToken();
|
|
106
119
|
const userId = config.get('userId');
|
|
@@ -108,6 +121,7 @@ async function connectTunnelClient(tunnelId, subdomain, localPort, silent = fals
|
|
|
108
121
|
const ws = new WebSocket(tunnelServerUrl);
|
|
109
122
|
|
|
110
123
|
let heartbeatInterval;
|
|
124
|
+
const isTcpTunnel = protocol === 'tcp' || protocol === 'ssh';
|
|
111
125
|
|
|
112
126
|
ws.on('open', () => {
|
|
113
127
|
if (!silent) console.log(chalk.green('✓ Connected to tunnel server'));
|
|
@@ -117,6 +131,7 @@ async function connectTunnelClient(tunnelId, subdomain, localPort, silent = fals
|
|
|
117
131
|
tunnelId,
|
|
118
132
|
subdomain,
|
|
119
133
|
localPort,
|
|
134
|
+
protocol,
|
|
120
135
|
authToken: token,
|
|
121
136
|
userId
|
|
122
137
|
}));
|
|
@@ -137,8 +152,36 @@ async function connectTunnelClient(tunnelId, subdomain, localPort, silent = fals
|
|
|
137
152
|
console.log(chalk.green.bold('\n🎉 Tunnel Active!\n'));
|
|
138
153
|
console.log(chalk.white('Your local server is now accessible at:'));
|
|
139
154
|
console.log(chalk.cyan.bold(` ${message.publicUrl}\n`));
|
|
155
|
+
|
|
156
|
+
// Show TCP/SSH specific info
|
|
157
|
+
if (isTcpTunnel && message.tcpHost && message.tcpPort) {
|
|
158
|
+
console.log(chalk.yellow.bold('🔌 TCP/SSH Connection Info:\n'));
|
|
159
|
+
console.log(chalk.white(` TCP Host: ${message.tcpHost}`));
|
|
160
|
+
console.log(chalk.white(` TCP Port: ${message.tcpPort}\n`));
|
|
161
|
+
if (message.sshCommand) {
|
|
162
|
+
console.log(chalk.gray('SSH Command (Linux/Mac):'));
|
|
163
|
+
console.log(chalk.cyan(` ${message.sshCommand}\n`));
|
|
164
|
+
console.log(chalk.gray('SSH Command (Windows with ncat):'));
|
|
165
|
+
console.log(chalk.cyan(` ssh -o ProxyCommand="ncat ${message.tcpHost} ${message.tcpPort}" user@${message.tcpHost}\n`));
|
|
166
|
+
console.log(chalk.gray('SSH with Key (passwordless):'));
|
|
167
|
+
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`));
|
|
168
|
+
}
|
|
169
|
+
} else {
|
|
170
|
+
// Display QR code for HTTP/HTTPS tunnels (not for TCP/SSH)
|
|
171
|
+
displayQRCode(message.publicUrl);
|
|
172
|
+
}
|
|
173
|
+
|
|
140
174
|
console.log(chalk.gray('Press Ctrl+C to stop the tunnel\n'));
|
|
141
175
|
}
|
|
176
|
+
} else if (message.type === 'tcp-connect') {
|
|
177
|
+
// Handle new TCP connection for SSH/TCP tunnels
|
|
178
|
+
handleTcpConnect(message, localPort, ws, silent);
|
|
179
|
+
} else if (message.type === 'tcp-data') {
|
|
180
|
+
// Handle TCP data from tunnel server
|
|
181
|
+
handleTcpData(message, ws, silent);
|
|
182
|
+
} else if (message.type === 'tcp-end') {
|
|
183
|
+
// Handle TCP connection close
|
|
184
|
+
handleTcpEnd(message, silent);
|
|
142
185
|
} else if (message.type === 'request') {
|
|
143
186
|
if (!silent) {
|
|
144
187
|
const timestamp = new Date().toLocaleTimeString();
|
|
@@ -155,15 +198,34 @@ async function connectTunnelClient(tunnelId, subdomain, localPort, silent = fals
|
|
|
155
198
|
delete forwardHeaders['connection'];
|
|
156
199
|
delete forwardHeaders['accept-encoding']; // Prevent compression issues
|
|
157
200
|
|
|
201
|
+
// Handle body - check if it's base64 encoded (binary/multipart data)
|
|
202
|
+
let requestBody = message.body;
|
|
203
|
+
if (message.body && typeof message.body === 'object' && message.body._isBase64) {
|
|
204
|
+
// Decode base64 body back to Buffer
|
|
205
|
+
requestBody = Buffer.from(message.body.data, 'base64');
|
|
206
|
+
if (!silent) {
|
|
207
|
+
console.log(chalk.gray(` 📎 Decoded ${message.body._isMultipart ? 'multipart' : 'binary'} body: ${requestBody.length} bytes`));
|
|
208
|
+
}
|
|
209
|
+
// For multipart, keep the original content-length
|
|
210
|
+
if (!message.body._isMultipart) {
|
|
211
|
+
delete forwardHeaders['content-length'];
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
// For non-binary data, let axios calculate content-length
|
|
215
|
+
delete forwardHeaders['content-length'];
|
|
216
|
+
}
|
|
217
|
+
|
|
158
218
|
const response = await axios({
|
|
159
219
|
method: message.method.toLowerCase(),
|
|
160
220
|
url: localUrl,
|
|
161
221
|
headers: forwardHeaders,
|
|
162
|
-
data:
|
|
222
|
+
data: requestBody,
|
|
163
223
|
validateStatus: () => true, // Accept any status code
|
|
164
224
|
responseType: 'arraybuffer', // Handle binary data properly
|
|
165
225
|
maxRedirects: 0, // Don't follow redirects, let the client handle them
|
|
166
|
-
timeout: 25000 // 25 second timeout
|
|
226
|
+
timeout: 25000, // 25 second timeout
|
|
227
|
+
maxBodyLength: Infinity, // Allow large file uploads
|
|
228
|
+
maxContentLength: Infinity // Allow large responses
|
|
167
229
|
});
|
|
168
230
|
|
|
169
231
|
// Clean up headers to avoid conflicts
|
|
@@ -503,6 +565,101 @@ async function configureIngress(tunnelId, rules, options) {
|
|
|
503
565
|
}
|
|
504
566
|
}
|
|
505
567
|
|
|
568
|
+
// TCP connection handlers for SSH/TCP tunnels
|
|
569
|
+
function handleTcpConnect(message, localPort, ws, silent) {
|
|
570
|
+
const { connectionId, remoteAddress, remotePort } = message;
|
|
571
|
+
|
|
572
|
+
if (!silent) {
|
|
573
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
574
|
+
console.log(chalk.gray(`[${timestamp}]`), chalk.magenta('TCP'), chalk.white(`Connection from ${remoteAddress}:${remotePort}`));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// Create connection to local server (e.g., SSH on port 22)
|
|
578
|
+
const localSocket = net.createConnection({
|
|
579
|
+
host: 'localhost',
|
|
580
|
+
port: localPort
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
tcpConnections.set(connectionId, localSocket);
|
|
584
|
+
|
|
585
|
+
localSocket.on('connect', () => {
|
|
586
|
+
if (!silent) {
|
|
587
|
+
console.log(chalk.green(` ✓ Connected to local port ${localPort}`));
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
localSocket.on('data', (data) => {
|
|
592
|
+
// Forward data back to tunnel server
|
|
593
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
594
|
+
ws.send(JSON.stringify({
|
|
595
|
+
type: 'tcp-response',
|
|
596
|
+
connectionId,
|
|
597
|
+
data: data.toString('base64')
|
|
598
|
+
}));
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
localSocket.on('end', () => {
|
|
603
|
+
if (!silent) {
|
|
604
|
+
console.log(chalk.gray(` TCP connection ended: ${connectionId.substring(0, 8)}...`));
|
|
605
|
+
}
|
|
606
|
+
tcpConnections.delete(connectionId);
|
|
607
|
+
|
|
608
|
+
// Notify tunnel server
|
|
609
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
610
|
+
ws.send(JSON.stringify({
|
|
611
|
+
type: 'tcp-close',
|
|
612
|
+
connectionId
|
|
613
|
+
}));
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
localSocket.on('error', (error) => {
|
|
618
|
+
console.error(chalk.red(` TCP error: ${error.message}`));
|
|
619
|
+
tcpConnections.delete(connectionId);
|
|
620
|
+
|
|
621
|
+
// Notify tunnel server
|
|
622
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
623
|
+
ws.send(JSON.stringify({
|
|
624
|
+
type: 'tcp-close',
|
|
625
|
+
connectionId
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function handleTcpData(message, ws, silent) {
|
|
632
|
+
const { connectionId, data } = message;
|
|
633
|
+
|
|
634
|
+
const localSocket = tcpConnections.get(connectionId);
|
|
635
|
+
if (!localSocket) {
|
|
636
|
+
if (!silent) {
|
|
637
|
+
console.warn(chalk.yellow(` No local socket found for ${connectionId.substring(0, 8)}...`));
|
|
638
|
+
}
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
try {
|
|
643
|
+
const buffer = Buffer.from(data, 'base64');
|
|
644
|
+
localSocket.write(buffer);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
console.error(chalk.red(` Error writing to local socket: ${error.message}`));
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function handleTcpEnd(message, silent) {
|
|
651
|
+
const { connectionId } = message;
|
|
652
|
+
|
|
653
|
+
const localSocket = tcpConnections.get(connectionId);
|
|
654
|
+
if (localSocket) {
|
|
655
|
+
localSocket.end();
|
|
656
|
+
tcpConnections.delete(connectionId);
|
|
657
|
+
if (!silent) {
|
|
658
|
+
console.log(chalk.gray(` TCP connection closed: ${connectionId.substring(0, 8)}...`));
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
506
663
|
module.exports = {
|
|
507
664
|
start,
|
|
508
665
|
list,
|
|
@@ -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.
|
|
3
|
+
"version": "2.6.0",
|
|
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/",
|
|
@@ -39,13 +62,17 @@
|
|
|
39
62
|
"conf": "^10.2.0",
|
|
40
63
|
"dotenv": "^16.3.1",
|
|
41
64
|
"inquirer": "^8.2.5",
|
|
65
|
+
"mime-types": "^3.0.2",
|
|
42
66
|
"open": "^8.4.0",
|
|
43
67
|
"ora": "^5.4.1",
|
|
68
|
+
"qrcode-terminal": "^0.12.0",
|
|
44
69
|
"update-notifier": "^6.0.2",
|
|
45
70
|
"ws": "^8.14.2"
|
|
46
71
|
},
|
|
47
72
|
"devDependencies": {
|
|
48
|
-
"
|
|
73
|
+
"@yao-pkg/pkg": "^5.11.0",
|
|
74
|
+
"jest": "^29.7.0",
|
|
75
|
+
"rcedit": "^5.0.2"
|
|
49
76
|
},
|
|
50
77
|
"engines": {
|
|
51
78
|
"node": ">=14.0.0"
|
package/utils/api.js
CHANGED
|
@@ -109,6 +109,17 @@ class APIClient {
|
|
|
109
109
|
return response.data;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
// File Transfer
|
|
113
|
+
async checkFileTransferLimit() {
|
|
114
|
+
const response = await this.client.get('/tunnels/file-transfer/check');
|
|
115
|
+
return response.data;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async trackFileTransfer(userId, bytes) {
|
|
119
|
+
const response = await this.client.post('/tunnels/file-transfer/track', { userId, bytes });
|
|
120
|
+
return response.data;
|
|
121
|
+
}
|
|
122
|
+
|
|
112
123
|
// Webhooks
|
|
113
124
|
async createWebhook(data) {
|
|
114
125
|
const response = await this.client.post('/webhooks', data);
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
|
|
3
|
+
class E2EEncryptionClient {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.algorithm = 'aes-256-gcm';
|
|
6
|
+
this.keyLength = 32;
|
|
7
|
+
this.ivLength = 16;
|
|
8
|
+
this.authTagLength = 16;
|
|
9
|
+
this.ecdh = null;
|
|
10
|
+
this.publicKey = null;
|
|
11
|
+
this.privateKey = null;
|
|
12
|
+
this.sharedSecret = null;
|
|
13
|
+
this.serverPublicKey = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Initialize client-side encryption
|
|
17
|
+
initialize() {
|
|
18
|
+
this.ecdh = crypto.createECDH('secp256k1');
|
|
19
|
+
this.ecdh.generateKeys();
|
|
20
|
+
this.publicKey = this.ecdh.getPublicKey('base64');
|
|
21
|
+
this.privateKey = this.ecdh.getPrivateKey('base64');
|
|
22
|
+
return this.publicKey;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Set server public key and compute shared secret
|
|
26
|
+
setServerPublicKey(serverPublicKey) {
|
|
27
|
+
this.serverPublicKey = serverPublicKey;
|
|
28
|
+
const publicKeyBuffer = Buffer.from(serverPublicKey, 'base64');
|
|
29
|
+
const sharedSecret = this.ecdh.computeSecret(publicKeyBuffer);
|
|
30
|
+
this.sharedSecret = crypto.createHash('sha256').update(sharedSecret).digest();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Check if E2E is ready
|
|
35
|
+
isReady() {
|
|
36
|
+
return this.sharedSecret !== null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get client public key
|
|
40
|
+
getPublicKey() {
|
|
41
|
+
return this.publicKey;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Encrypt data
|
|
45
|
+
encrypt(plaintext) {
|
|
46
|
+
if (!this.sharedSecret) {
|
|
47
|
+
throw new Error('E2E encryption not initialized');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
51
|
+
const cipher = crypto.createCipheriv(this.algorithm, this.sharedSecret, iv, {
|
|
52
|
+
authTagLength: this.authTagLength
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
let encrypted = cipher.update(plaintext, 'utf8', 'base64');
|
|
56
|
+
encrypted += cipher.final('base64');
|
|
57
|
+
const authTag = cipher.getAuthTag();
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
encrypted,
|
|
61
|
+
iv: iv.toString('base64'),
|
|
62
|
+
authTag: authTag.toString('base64')
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Decrypt data
|
|
67
|
+
decrypt(encryptedData) {
|
|
68
|
+
if (!this.sharedSecret) {
|
|
69
|
+
throw new Error('E2E encryption not initialized');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { encrypted, iv, authTag } = encryptedData;
|
|
73
|
+
const ivBuffer = Buffer.from(iv, 'base64');
|
|
74
|
+
const authTagBuffer = Buffer.from(authTag, 'base64');
|
|
75
|
+
|
|
76
|
+
const decipher = crypto.createDecipheriv(this.algorithm, this.sharedSecret, ivBuffer, {
|
|
77
|
+
authTagLength: this.authTagLength
|
|
78
|
+
});
|
|
79
|
+
decipher.setAuthTag(authTagBuffer);
|
|
80
|
+
|
|
81
|
+
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
|
82
|
+
decrypted += decipher.final('utf8');
|
|
83
|
+
|
|
84
|
+
return decrypted;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Encrypt binary data
|
|
88
|
+
encryptBinary(buffer) {
|
|
89
|
+
if (!this.sharedSecret) {
|
|
90
|
+
throw new Error('E2E encryption not initialized');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
94
|
+
const cipher = crypto.createCipheriv(this.algorithm, this.sharedSecret, iv, {
|
|
95
|
+
authTagLength: this.authTagLength
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const encrypted = Buffer.concat([cipher.update(buffer), cipher.final()]);
|
|
99
|
+
const authTag = cipher.getAuthTag();
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
encrypted: encrypted.toString('base64'),
|
|
103
|
+
iv: iv.toString('base64'),
|
|
104
|
+
authTag: authTag.toString('base64')
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Decrypt binary data
|
|
109
|
+
decryptBinary(encryptedData) {
|
|
110
|
+
if (!this.sharedSecret) {
|
|
111
|
+
throw new Error('E2E encryption not initialized');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const { encrypted, iv, authTag } = encryptedData;
|
|
115
|
+
const encryptedBuffer = Buffer.from(encrypted, 'base64');
|
|
116
|
+
const ivBuffer = Buffer.from(iv, 'base64');
|
|
117
|
+
const authTagBuffer = Buffer.from(authTag, 'base64');
|
|
118
|
+
|
|
119
|
+
const decipher = crypto.createDecipheriv(this.algorithm, this.sharedSecret, ivBuffer, {
|
|
120
|
+
authTagLength: this.authTagLength
|
|
121
|
+
});
|
|
122
|
+
decipher.setAuthTag(authTagBuffer);
|
|
123
|
+
|
|
124
|
+
return Buffer.concat([decipher.update(encryptedBuffer), decipher.final()]);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Encrypt a message object
|
|
128
|
+
encryptMessage(message) {
|
|
129
|
+
if (!this.isReady()) {
|
|
130
|
+
return message;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const plaintext = JSON.stringify(message);
|
|
134
|
+
const encrypted = this.encrypt(plaintext);
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
type: 'e2e-encrypted',
|
|
138
|
+
payload: encrypted
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Decrypt a message object
|
|
143
|
+
decryptMessage(encryptedMessage) {
|
|
144
|
+
if (encryptedMessage.type !== 'e2e-encrypted') {
|
|
145
|
+
return encryptedMessage;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const decrypted = this.decrypt(encryptedMessage.payload);
|
|
149
|
+
return JSON.parse(decrypted);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Reset encryption state
|
|
153
|
+
reset() {
|
|
154
|
+
this.ecdh = null;
|
|
155
|
+
this.publicKey = null;
|
|
156
|
+
this.privateKey = null;
|
|
157
|
+
this.sharedSecret = null;
|
|
158
|
+
this.serverPublicKey = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = E2EEncryptionClient;
|