webtun 1.4.2 → 1.4.5

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  ![Desktop](https://img.shields.io/badge/Desktop-Electron-47848F?style=for-the-badge)
7
7
  ![License](https://img.shields.io/badge/License-MIT-green?style=for-the-badge)
8
8
 
9
- **Access your Linux server from any browser — no VPN, no SSH client, no installing anything.**
9
+ **Access your server from any browser — no VPN, no SSH client, no installing anything.**
10
10
 
11
11
  [![Quick Start](https://img.shields.io/badge/Quick_Start-One_Command-2D5B8E?style=for-the-badge&logo=gnu-bash)](install.sh)
12
12
  [![Download](https://img.shields.io/github/v/release/unn-known1/webtun?style=for-the-badge&logo=github)](https://github.com/unn-known1/webtun/releases/latest)
@@ -97,6 +97,17 @@ webtun
97
97
  | `--help` | Show help |
98
98
  | `--version` | Show version |
99
99
 
100
+ **Environment Variables:**
101
+ | Variable | Description |
102
+ |----------|-------------|
103
+ | `PORT` | Server port (default: 3000) |
104
+ | `HOST` | Bind address (default: 0.0.0.0) |
105
+ | `PIN` | Authentication PIN (empty = no auth) |
106
+ | `SHELL` | Shell to use (default: PowerShell on Windows, bash/sh elsewhere) |
107
+ | `WORKSPACE_ROOT` | Root directory for file operations (default: ~) |
108
+ | `TRUST_PROXY` | Set to `true` if behind a reverse proxy (default: loopback only) |
109
+ | `WEBTUN_SHELL` | Override shell on Windows (e.g., `/usr/bin/bash` for Git Bash) |
110
+
100
111
  **Examples:**
101
112
  ```bash
102
113
  npx webtun # Start on port 3000
@@ -148,35 +159,35 @@ npm start
148
159
  ## Architecture
149
160
 
150
161
  ```
151
- ┌─────────────────────────────────────────────────────────────┐
152
- Your Browser
153
- ┌─────────┐ ┌──────────┐ ┌────────────┐ ┌────────────┐
154
- │ │Terminal │ │ File │ │ Tunnel │ │ Settings
155
- │ │ (xterm) │ │ Explorer │ │ Manager │ │ Panel
156
- └────┬────┘ └────┬─────┘ └──────┬─────┘ └─────┬──────┘
157
- │ │ │ │
158
- └───────┼────────────┼───────────────┼──────────────┼─────────┘
159
- WebSocket │ │
160
- ▼ ▼
161
- ┌─────────────────────────────────────────────────────────────┐
162
- Node.js Server
163
- ┌────────────┐ ┌─────────────┐ ┌────────────────────┐
164
- │ │ WebSocket │ │ File API │ Cloudflare Tunnel
165
- │ │ Handler │ │ (read/write)│ │ Manager
166
- └─────┬──────┘ └──────┬──────┘ └─────────┬──────────┘
167
- │ │
168
- ▼ ▼
169
- ┌─────────┐ ┌────────────────┐ ┌──────────────────────┐ │
170
- │ │ node-pty│ │Local Filesystem│ │ cloudflared daemon │ │
171
- │ │ (shell) │ │(your server) │ │ (exposes to internet)│ │
172
- └─────────┘ └────────────────┘ └──────────────────────┘ │
173
- └─────────────────────────────────────────────────────────────┘
174
-
175
-
176
- ┌─────────────────┐
177
- │ Internet Users │
178
- │ (HTTPS URL) │
179
- └─────────────────┘
162
+ ┌────────────────────────────────────────────────────────────────┐
163
+ Your Browser
164
+ │ ┌──────────┐ ┌───────────┐ ┌─────────────┐ ┌───────────┐
165
+ │ │ Terminal │ │ File │ │ Tunnel │ │ Settings
166
+ │ │ (xterm) │ │ Explorer │ │ Manager │ │ Panel
167
+ └─────┬────┘ └─────┬─────┘ └──────┬──────┘ └─────┬─────┘
168
+ │ │ │ │
169
+ └────────┼─────────────┼───────────────┼───────────────┼─────────┘
170
+ WebSocket │ REST API │
171
+ ▼ ▼
172
+ ┌────────────────────────────────────────────────────────────────┐
173
+ Node.js Server
174
+ │ ┌─────────────┐ ┌──────────────┐ ┌───────────────────────┐
175
+ │ │ WebSocket │ │ File API │ Cloudflare Tunnel │
176
+ │ │ Handler │ │ (read/write) │ │ Manager
177
+ │ └──────┬──────┘ └──────┬───────┘ └───────────┬───────────┘
178
+ │ │
179
+ ▼ ▼
180
+ ┌──────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
181
+ │ │ node-pty │ │ Local Filesystem │ │ cloudflared daemon │ │
182
+ │ │ (shell) │ │ (your server) │ │ (exposes to internet)│ │
183
+ └──────────┘ └──────────────────┘ └──────────────────────┘ │
184
+ └────────────────────────────────────────────────────────────────┘
185
+
186
+
187
+ ┌─────────────────┐
188
+ │ Internet Users │
189
+ │ (HTTPS URL) │
190
+ └─────────────────┘
180
191
  ```
181
192
 
182
193
  ---
@@ -231,6 +242,9 @@ kill $(pgrep -f 'cloudflared tunnel')
231
242
  | Permission denied on shell | Ensure user has shell access: `chsh -s /bin/bash` |
232
243
  | File upload fails | Check `public/uploads/` permissions: `chmod 755 public/uploads/` |
233
244
  | Port 3000 in use | Change port: `PORT=3001 npm start` |
245
+ | Windows terminal opens PowerShell instead of Git Bash | Set `WEBTUN_SHELL=/usr/bin/bash` in `.env` |
246
+ | Behind reverse proxy, wrong client IP | Set `TRUST_PROXY=true` in `.env` |
247
+ | Tunnel persists after server restart | Tunnels are auto-managed; manual cleanup: `kill $(pgrep -f 'cloudflared tunnel')` |
234
248
 
235
249
  ---
236
250
 
package/bin/webtun.js CHANGED
@@ -23,7 +23,7 @@ function printHelp() {
23
23
  PORT Server port (default: 3000)
24
24
  HOST Bind address (default: 0.0.0.0)
25
25
  PIN Authentication PIN (empty = no auth)
26
- SHELL Shell to use (default: /bin/bash or system shell)
26
+ SHELL Shell to use (default: PowerShell on Windows, bash/sh elsewhere)
27
27
  WORKSPACE_ROOT Root directory for file operations (default: ~)
28
28
 
29
29
  Examples:
@@ -70,25 +70,20 @@ function parseArgs(argv) {
70
70
  }
71
71
 
72
72
  function startTunnel(port) {
73
- const { execSync, spawn } = require('child_process');
74
- const os = require('os');
73
+ const { spawn } = require('child_process');
74
+ const { findCloudflared } = require('../server');
75
75
 
76
- // Check if cloudflared is installed
77
- try {
78
- if (os.platform() === 'win32') {
79
- execSync('where cloudflared', { stdio: 'ignore' });
80
- } else {
81
- execSync('command -v cloudflared', { stdio: 'ignore' });
82
- }
83
- } catch {
76
+ const bin = findCloudflared();
77
+ if (!bin) {
84
78
  console.error('\n Error: cloudflared is not installed.');
85
79
  console.error(' Install it from: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/');
80
+ console.error(' Or re-run: npm install webtun (postinstall downloads it)');
86
81
  return;
87
82
  }
88
83
 
89
- console.log('\n Starting Cloudflare Tunnel...');
84
+ console.log(' Starting Cloudflare Tunnel...');
90
85
 
91
- const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
86
+ const proc = spawn(bin, ['tunnel', '--url', `http://localhost:${port}`], {
92
87
  stdio: ['ignore', 'pipe', 'pipe']
93
88
  });
94
89
 
@@ -121,23 +116,22 @@ function startTunnel(port) {
121
116
  }
122
117
  });
123
118
 
124
- // Cleanup on exit
125
- process.on('SIGINT', () => {
126
- try { proc.kill('SIGTERM'); } catch {}
127
- process.exit(0);
128
- });
129
- process.on('SIGTERM', () => {
119
+ const stop = () => {
130
120
  try { proc.kill('SIGTERM'); } catch {}
131
121
  process.exit(0);
132
- });
122
+ };
123
+ process.on('SIGINT', stop);
124
+ process.on('SIGTERM', stop);
133
125
  }
134
126
 
135
127
  const opts = parseArgs(args);
136
- const { startServer } = require('../server');
128
+ const { startServer, PORT } = require('../server');
129
+
130
+ const listenPort = opts.port || PORT;
137
131
 
138
132
  startServer(opts).then(() => {
139
133
  if (opts.tunnel) {
140
- startTunnel(opts.port || 3000);
134
+ startTunnel(listenPort);
141
135
  }
142
136
  }).catch(err => {
143
137
  console.error('Failed to start server:', err.message);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webtun",
3
- "version": "1.4.2",
3
+ "version": "1.4.5",
4
4
  "description": "Self-hosted web terminal with Cloudflare Tunnel, file explorer, and PWA support",
5
5
  "author": {
6
6
  "name": "Gaurang Patel",
@@ -93,10 +93,12 @@
93
93
  }
94
94
  },
95
95
  "dependencies": {
96
+ "archiver": "^8.0.0",
96
97
  "express": "^4.18.2",
97
98
  "multer": "^2.0.0",
98
99
  "node-pty": "^1.0.0",
99
- "ws": "^8.21.0"
100
+ "ws": "^8.21.0",
101
+ "yauzl": "^3.4.0"
100
102
  },
101
103
  "devDependencies": {
102
104
  "electron": "^42.2.0",
@@ -105,5 +107,8 @@
105
107
  },
106
108
  "overrides": {
107
109
  "form-data": "4.0.6"
110
+ },
111
+ "allowScripts": {
112
+ "node-pty@1.1.0": true
108
113
  }
109
114
  }
package/postinstall.js CHANGED
@@ -2,6 +2,8 @@ const { execSync, spawnSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
+ const https = require('https');
6
+ const http = require('http');
5
7
 
6
8
  // ── Rebuild node-pty if native module is missing ─────────────────────
7
9
  function rebuildNodePty() {
@@ -18,16 +20,20 @@ function rebuildNodePty() {
18
20
  spawnSync('npm', ['rebuild', 'node-pty'], {
19
21
  cwd: __dirname,
20
22
  stdio: 'inherit',
21
- timeout: 120000
23
+ timeout: 120000,
24
+ shell: os.platform() === 'win32'
22
25
  });
23
26
  // Verify it worked
27
+ delete require.cache[require.resolve('node-pty')];
24
28
  require('node-pty');
25
29
  console.log(' node-pty rebuilt successfully');
26
30
  } catch (e) {
27
31
  console.log(' node-pty rebuild failed: ' + e.message);
28
- console.log(' Terminal requires build tools: python3, make, g++');
29
- console.log(' Linux: sudo apt-get install -y python3 make g++');
30
- console.log(' macOS: xcode-select --install');
32
+ console.log(' Terminal requires native build tools:');
33
+ console.log(' Linux: sudo apt-get install -y python3 make g++');
34
+ console.log(' macOS: xcode-select --install');
35
+ console.log(' Windows: install Visual Studio Build Tools with "Desktop development with C++"');
36
+ console.log(' https://visualstudio.microsoft.com/visual-cpp-build-tools/');
31
37
  }
32
38
  }
33
39
 
@@ -36,47 +42,101 @@ rebuildNodePty();
36
42
  // ── Install cloudflared ──────────────────────────────────────────────
37
43
  const CF_RELEASES = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
38
44
 
39
- try {
40
- if (os.platform() === 'win32') {
41
- execSync('where cloudflared', { stdio: 'ignore' });
45
+ function cloudflaredExists() {
46
+ try {
47
+ if (os.platform() === 'win32') {
48
+ execSync('where cloudflared', { stdio: 'ignore' });
49
+ } else {
50
+ execSync('command -v cloudflared', { stdio: 'ignore' });
51
+ }
52
+ return true;
53
+ } catch {}
54
+
55
+ const isWin = os.platform() === 'win32';
56
+ const name = isWin ? 'cloudflared.exe' : 'cloudflared';
57
+ const candidates = [
58
+ path.join(__dirname, name),
59
+ path.join(process.cwd(), name),
60
+ ];
61
+ if (isWin) {
62
+ const localApp = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
63
+ candidates.push(path.join(localApp, 'cloudflared', name));
64
+ const pf = process.env.ProgramW6432 || process.env.ProgramFiles;
65
+ if (pf) candidates.push(path.join(pf, 'cloudflared', name));
42
66
  } else {
43
- execSync('command -v cloudflared', { stdio: 'ignore' });
67
+ candidates.push(path.join(os.homedir(), '.local', 'bin', name));
68
+ candidates.push('/usr/local/bin/' + name);
44
69
  }
45
- } catch {
46
- // cloudflared not found install it
47
- const platform = os.platform();
48
- const arch = os.arch();
70
+ return candidates.some(c => {
71
+ try { return fs.existsSync(c) && fs.statSync(c).isFile(); } catch { return false; }
72
+ });
73
+ }
49
74
 
50
- const files = {
51
- linux: { x64: 'cloudflared-linux-amd64', arm64: 'cloudflared-linux-arm64', arm: 'cloudflared-linux-arm' },
52
- darwin: { x64: 'cloudflared-darwin-amd64.tgz', arm64: 'cloudflared-darwin-arm64.tgz' },
53
- win32: { x64: 'cloudflared-windows-amd64.exe' }
54
- };
75
+ if (cloudflaredExists()) process.exit(0);
55
76
 
56
- const file = (files[platform] || {})[arch];
57
- if (!file) process.exit(0);
77
+ const platform = os.platform();
78
+ const arch = os.arch();
58
79
 
59
- console.log(' installing cloudflared...');
80
+ const files = {
81
+ linux: { x64: 'cloudflared-linux-amd64', arm64: 'cloudflared-linux-arm64', arm: 'cloudflared-linux-arm' },
82
+ darwin: { x64: 'cloudflared-darwin-amd64.tgz', arm64: 'cloudflared-darwin-arm64.tgz' },
83
+ win32: { x64: 'cloudflared-windows-amd64.exe', arm64: 'cloudflared-windows-amd64.exe' }
84
+ };
60
85
 
61
- const url = CF_RELEASES + '/' + file;
62
- const tmpDir = os.tmpdir();
63
- const tmp = path.join(tmpDir, 'cloudflared-' + process.pid + (platform === 'win32' ? '.exe' : ''));
86
+ const file = (files[platform] || {})[arch];
87
+ if (!file) process.exit(0);
64
88
 
65
- function cleanup() {
66
- try { fs.unlinkSync(tmp); } catch {}
67
- }
89
+ console.log(' installing cloudflared...');
68
90
 
69
- function download() {
70
- const r = spawnSync('curl', ['-#fL', url, '-o', tmp], { stdio: 'inherit', timeout: 60000 });
71
- if (r.status === 0) return true;
72
- const r2 = spawnSync('wget', ['-q', url, '-O', tmp], { stdio: 'inherit', timeout: 60000 });
73
- return r2.status === 0;
74
- }
91
+ const url = CF_RELEASES + '/' + file;
92
+ const tmpDir = os.tmpdir();
93
+ const tmp = path.join(tmpDir, 'cloudflared-' + process.pid + (platform === 'win32' ? '.exe' : ''));
75
94
 
76
- if (!download()) {
77
- console.log(' cloudflared install failed (download error) skipping');
78
- cleanup();
79
- process.exit(0);
95
+ function cleanup() {
96
+ try { fs.unlinkSync(tmp); } catch {}
97
+ }
98
+
99
+ function downloadFile(downloadUrl, dest) {
100
+ return new Promise((resolve, reject) => {
101
+ const mod = downloadUrl.startsWith('https') ? https : http;
102
+ const req = mod.get(downloadUrl, { headers: { 'User-Agent': 'webtun-postinstall' } }, res => {
103
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
104
+ res.resume();
105
+ return downloadFile(res.headers.location, dest).then(resolve, reject);
106
+ }
107
+ if (res.statusCode !== 200) {
108
+ res.resume();
109
+ return reject(new Error('HTTP ' + res.statusCode));
110
+ }
111
+ const out = fs.createWriteStream(dest);
112
+ res.pipe(out);
113
+ out.on('finish', () => out.close(err => err ? reject(err) : resolve()));
114
+ out.on('error', reject);
115
+ });
116
+ req.on('error', reject);
117
+ req.setTimeout(60000, () => {
118
+ req.destroy();
119
+ reject(new Error('download timeout'));
120
+ });
121
+ });
122
+ }
123
+
124
+ function downloadWithCurlOrWget() {
125
+ const r = spawnSync('curl', ['-#fL', url, '-o', tmp], { stdio: 'inherit', timeout: 60000 });
126
+ if (r.status === 0) return true;
127
+ const r2 = spawnSync('wget', ['-q', url, '-O', tmp], { stdio: 'inherit', timeout: 60000 });
128
+ return r2.status === 0;
129
+ }
130
+
131
+ async function main() {
132
+ try {
133
+ await downloadFile(url, tmp);
134
+ } catch (e) {
135
+ if (!downloadWithCurlOrWget()) {
136
+ console.log(' cloudflared install failed (download error) — skipping');
137
+ cleanup();
138
+ return;
139
+ }
80
140
  }
81
141
 
82
142
  // Validate downloaded file is not HTML (e.g. 404 page)
@@ -89,15 +149,16 @@ try {
89
149
  if (/^<!doctype\s+html/i.test(head) || /^<html/i.test(head)) {
90
150
  console.log(' cloudflared install failed (downloaded file is not a binary) — skipping');
91
151
  cleanup();
92
- process.exit(0);
152
+ return;
93
153
  }
94
154
  } catch (e) {
95
155
  console.log(' cloudflared install failed (cannot validate download): ' + e.message);
96
156
  cleanup();
97
- process.exit(0);
157
+ return;
98
158
  }
99
159
 
100
160
  function installBin(src, dest) {
161
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
101
162
  try {
102
163
  fs.renameSync(src, dest);
103
164
  } catch (e) {
@@ -111,35 +172,61 @@ try {
111
172
  try { fs.chmodSync(dest, 0o755); } catch {}
112
173
  }
113
174
 
175
+ function preferredDest() {
176
+ const name = platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
177
+ if (platform === 'win32') {
178
+ const localApp = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
179
+ return path.join(localApp, 'cloudflared', name);
180
+ }
181
+ return path.join(os.homedir(), '.local', 'bin', name);
182
+ }
183
+
184
+ function extractDarwinIfNeeded() {
185
+ if (platform !== 'darwin') return tmp;
186
+ const result = spawnSync('tar', ['xzf', tmp, '-C', tmpDir], { stdio: 'inherit' });
187
+ if (result.status !== 0) throw new Error('tar extraction failed');
188
+ try { fs.unlinkSync(tmp); } catch {}
189
+ return path.join(tmpDir, 'cloudflared');
190
+ }
191
+
114
192
  try {
115
- if (platform === 'darwin') {
116
- const result = spawnSync('tar', ['xzf', tmp, '-C', tmpDir], { stdio: 'inherit' });
117
- if (result.status !== 0) throw new Error('tar extraction failed');
118
- installBin(path.join(tmpDir, 'cloudflared'), '/usr/local/bin/cloudflared');
119
- try { fs.unlinkSync(tmp); } catch {}
120
- } else if (platform === 'win32') {
121
- const progFiles = process.env.ProgramW6432 || process.env.ProgramFiles || (process.env.SystemRoot + '\\Program Files');
122
- const dest = path.join(progFiles, 'cloudflared', 'cloudflared.exe');
123
- fs.mkdirSync(path.dirname(dest), { recursive: true });
124
- installBin(tmp, dest);
125
- } else {
126
- installBin(tmp, '/usr/local/bin/cloudflared');
193
+ const src = extractDarwinIfNeeded();
194
+ // Prefer user-local path (no admin), then package dir, then system
195
+ const dests = [preferredDest(), path.join(__dirname, platform === 'win32' ? 'cloudflared.exe' : 'cloudflared')];
196
+ if (platform !== 'win32') dests.push('/usr/local/bin/cloudflared');
197
+ else {
198
+ const pf = process.env.ProgramW6432 || process.env.ProgramFiles;
199
+ if (pf) dests.push(path.join(pf, 'cloudflared', 'cloudflared.exe'));
127
200
  }
128
- console.log(' cloudflared installed');
129
- cleanup();
130
- } catch (e) {
131
- if (e.code === 'EACCES' || e.code === 'EPERM') {
132
- const localDest = path.join(process.cwd(), 'cloudflared' + (platform === 'win32' ? '.exe' : ''));
201
+
202
+ let installed = null;
203
+ let lastErr = null;
204
+ for (const dest of dests) {
133
205
  try {
134
- installBin(tmp, localDest);
135
- console.log(' cloudflared installed to ' + localDest);
136
- console.log(' Move it to your PATH: sudo mv ' + localDest + ' /usr/local/bin/');
137
- } catch (e2) {
138
- console.log(' cloudflared install failed: ' + e2.message + ' — skipping');
206
+ installBin(src, dest);
207
+ installed = dest;
208
+ break;
209
+ } catch (e) {
210
+ lastErr = e;
211
+ }
212
+ }
213
+
214
+ if (installed) {
215
+ console.log(' cloudflared installed → ' + installed);
216
+ if (platform === 'win32' && installed.includes('Local')) {
217
+ console.log(' (not on PATH; WebTun will find it automatically)');
139
218
  }
140
219
  } else {
141
- console.log(' cloudflared install failed: ' + e.message + ' — skipping');
220
+ console.log(' cloudflared install failed: ' + (lastErr && lastErr.message) + ' — skipping');
142
221
  }
143
222
  cleanup();
223
+ } catch (e) {
224
+ console.log(' cloudflared install failed: ' + e.message + ' — skipping');
225
+ cleanup();
144
226
  }
145
227
  }
228
+
229
+ main().catch(e => {
230
+ console.log(' cloudflared install failed: ' + e.message + ' — skipping');
231
+ cleanup();
232
+ });