webtun 1.4.0 → 1.4.1

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
@@ -76,6 +76,45 @@
76
76
 
77
77
  ## Quick Start
78
78
 
79
+ ### npm (Recommended)
80
+ ```bash
81
+ npx webtun
82
+ ```
83
+
84
+ Install globally:
85
+ ```bash
86
+ npm install -g webtun
87
+ webtun
88
+ ```
89
+
90
+ **Options:**
91
+ | Flag | Description |
92
+ |------|-------------|
93
+ | `--port, -p` | Port (default: 3000) |
94
+ | `--host, -h` | Host (default: 0.0.0.0) |
95
+ | `--pin` | Authentication PIN |
96
+ | `--tunnel, -t` | Start Cloudflare Tunnel |
97
+ | `--help` | Show help |
98
+ | `--version` | Show version |
99
+
100
+ **Examples:**
101
+ ```bash
102
+ npx webtun # Start on port 3000
103
+ npx webtun --port 8080 # Custom port
104
+ npx webtun --pin secret123 # With PIN protection
105
+ npx webtun --tunnel # With Cloudflare Tunnel
106
+ npx webtun -p 4000 -t # Port 4000 + tunnel
107
+ ```
108
+
109
+ **Note:** `node-pty` requires build tools on first install:
110
+ ```bash
111
+ # Debian/Ubuntu
112
+ sudo apt-get install -y python3 make g++
113
+
114
+ # macOS
115
+ xcode-select --install
116
+ ```
117
+
79
118
  ### One-Command Install
80
119
  ```bash
81
120
  bash -c "$(curl -fsSL https://raw.githubusercontent.com/unn-Known1/webtun/main/install.sh)"
package/bin/webtun.js CHANGED
@@ -15,6 +15,7 @@ function printHelp() {
15
15
  --port, -p <port> Port to listen on (default: 3000 or $PORT)
16
16
  --host, -h <host> Host to bind to (default: 0.0.0.0 or $HOST)
17
17
  --pin <pin> PIN for authentication (default: $PIN)
18
+ --tunnel, -t Start a Cloudflare Tunnel for remote access
18
19
  --help Show this help message
19
20
  --version Show version number
20
21
 
@@ -29,12 +30,14 @@ function printHelp() {
29
30
  webtun Start on default port
30
31
  webtun --port 8080 Start on port 8080
31
32
  webtun --pin secret123 Start with PIN protection
33
+ webtun --tunnel Start with Cloudflare Tunnel
34
+ webtun -p 4000 -t Port 4000 + tunnel
32
35
  PORT=4000 webtun Start on port 4000 via env var
33
36
  `);
34
37
  }
35
38
 
36
39
  function parseArgs(argv) {
37
- const opts = {};
40
+ const opts = { tunnel: false };
38
41
  for (let i = 0; i < argv.length; i++) {
39
42
  const arg = argv[i];
40
43
  if (arg === '--help' || arg === '-H') {
@@ -55,6 +58,8 @@ function parseArgs(argv) {
55
58
  opts.host = argv[++i];
56
59
  } else if (arg === '--pin') {
57
60
  process.env.PIN = argv[++i] || '';
61
+ } else if (arg === '--tunnel' || arg === '-t') {
62
+ opts.tunnel = true;
58
63
  } else {
59
64
  console.error(`Unknown option: ${arg}`);
60
65
  printHelp();
@@ -64,10 +69,77 @@ function parseArgs(argv) {
64
69
  return opts;
65
70
  }
66
71
 
72
+ function startTunnel(port) {
73
+ const { execSync, spawn } = require('child_process');
74
+ const os = require('os');
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 {
84
+ console.error('\n Error: cloudflared is not installed.');
85
+ console.error(' Install it from: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/');
86
+ return;
87
+ }
88
+
89
+ console.log('\n Starting Cloudflare Tunnel...');
90
+
91
+ const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
92
+ stdio: ['ignore', 'pipe', 'pipe']
93
+ });
94
+
95
+ let tunnelUrl = null;
96
+
97
+ const handler = data => {
98
+ const text = data.toString();
99
+ const m = text.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/);
100
+ if (m && !tunnelUrl) {
101
+ tunnelUrl = m[0];
102
+ console.log('');
103
+ console.log(' ┌─────────────────────────────────────────────────────┐');
104
+ console.log(' │ Public URL (share this!): │');
105
+ console.log(` │ ${tunnelUrl}`);
106
+ console.log(' └─────────────────────────────────────────────────────┘');
107
+ console.log('');
108
+ }
109
+ };
110
+
111
+ proc.stdout.on('data', handler);
112
+ proc.stderr.on('data', handler);
113
+
114
+ proc.on('error', (err) => {
115
+ console.error(' Tunnel error:', err.message);
116
+ });
117
+
118
+ proc.on('exit', (code) => {
119
+ if (code !== 0 && !tunnelUrl) {
120
+ console.error(' Tunnel exited with code', code);
121
+ }
122
+ });
123
+
124
+ // Cleanup on exit
125
+ process.on('SIGINT', () => {
126
+ try { proc.kill('SIGTERM'); } catch {}
127
+ process.exit(0);
128
+ });
129
+ process.on('SIGTERM', () => {
130
+ try { proc.kill('SIGTERM'); } catch {}
131
+ process.exit(0);
132
+ });
133
+ }
134
+
67
135
  const opts = parseArgs(args);
68
136
  const { startServer } = require('../server');
69
137
 
70
- startServer(opts).catch(err => {
138
+ startServer(opts).then(() => {
139
+ if (opts.tunnel) {
140
+ startTunnel(opts.port || 3000);
141
+ }
142
+ }).catch(err => {
71
143
  console.error('Failed to start server:', err.message);
72
144
  process.exit(1);
73
145
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webtun",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Self-hosted web terminal with Cloudflare Tunnel, file explorer, and PWA support",
5
5
  "author": {
6
6
  "name": "Gaurang Patel",
@@ -98,18 +98,12 @@
98
98
  "node-pty": "^1.0.0",
99
99
  "ws": "^8.21.0"
100
100
  },
101
- "optionalDependencies": {
102
- "electron": "^42.2.0",
103
- "electron-builder": "^26.8.1"
104
- },
105
101
  "devDependencies": {
102
+ "electron": "^42.2.0",
103
+ "electron-builder": "^26.8.1",
106
104
  "nodemon": "^3.1.14"
107
105
  },
108
106
  "overrides": {
109
107
  "form-data": "4.0.6"
110
- },
111
- "allowScripts": {
112
- "electron-winstaller@5.4.0": true,
113
- "node-pty@1.1.0": true
114
108
  }
115
109
  }
package/postinstall.js CHANGED
@@ -3,6 +3,37 @@ const fs = require('fs');
3
3
  const os = require('os');
4
4
  const path = require('path');
5
5
 
6
+ // ── Rebuild node-pty if native module is missing ─────────────────────
7
+ function rebuildNodePty() {
8
+ try {
9
+ require('node-pty');
10
+ return; // Already working
11
+ } catch {}
12
+
13
+ const ptyDir = path.join(__dirname, 'node_modules', 'node-pty');
14
+ if (!fs.existsSync(ptyDir)) return; // Not installed yet (first time npm install)
15
+
16
+ console.log(' rebuilding node-pty...');
17
+ try {
18
+ spawnSync('npm', ['rebuild', 'node-pty'], {
19
+ cwd: __dirname,
20
+ stdio: 'inherit',
21
+ timeout: 120000
22
+ });
23
+ // Verify it worked
24
+ require('node-pty');
25
+ console.log(' node-pty rebuilt successfully');
26
+ } catch (e) {
27
+ 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');
31
+ }
32
+ }
33
+
34
+ rebuildNodePty();
35
+
36
+ // ── Install cloudflared ──────────────────────────────────────────────
6
37
  const CF_RELEASES = 'https://github.com/cloudflare/cloudflared/releases/latest/download';
7
38
 
8
39
  try {
@@ -11,110 +42,104 @@ try {
11
42
  } else {
12
43
  execSync('command -v cloudflared', { stdio: 'ignore' });
13
44
  }
14
- process.exit(0);
15
- } catch {}
16
-
17
- const platform = os.platform();
18
- const arch = os.arch();
45
+ } catch {
46
+ // cloudflared not found — install it
47
+ const platform = os.platform();
48
+ const arch = os.arch();
19
49
 
20
- const files = {
21
- linux: { x64: 'cloudflared-linux-amd64', arm64: 'cloudflared-linux-arm64', arm: 'cloudflared-linux-arm' },
22
- darwin: { x64: 'cloudflared-darwin-amd64.tgz', arm64: 'cloudflared-darwin-arm64.tgz' },
23
- win32: { x64: 'cloudflared-windows-amd64.exe' }
24
- };
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
+ };
25
55
 
26
- const file = (files[platform] || {})[arch];
27
- if (!file) process.exit(0);
56
+ const file = (files[platform] || {})[arch];
57
+ if (!file) process.exit(0);
28
58
 
29
- console.log(' installing cloudflared...');
59
+ console.log(' installing cloudflared...');
30
60
 
31
- const url = CF_RELEASES + '/' + file;
32
- const tmpDir = os.tmpdir();
33
- const tmp = path.join(tmpDir, 'cloudflared-' + process.pid + (platform === 'win32' ? '.exe' : ''));
34
- const tmpExtracted = path.join(tmpDir, 'cloudflared-' + process.pid + '-bin' + (platform === 'win32' ? '.exe' : ''));
61
+ const url = CF_RELEASES + '/' + file;
62
+ const tmpDir = os.tmpdir();
63
+ const tmp = path.join(tmpDir, 'cloudflared-' + process.pid + (platform === 'win32' ? '.exe' : ''));
35
64
 
36
- function cleanup() {
37
- try { fs.unlinkSync(tmp); } catch {}
38
- try { fs.unlinkSync(tmpExtracted); } catch {}
39
- }
40
-
41
- function download() {
42
- const r = spawnSync('curl', ['-#fL', url, '-o', tmp], { stdio: 'inherit', timeout: 60000 });
43
- if (r.status === 0) return true;
44
- const r2 = spawnSync('wget', ['-q', url, '-O', tmp], { stdio: 'inherit', timeout: 60000 });
45
- return r2.status === 0;
46
- }
65
+ function cleanup() {
66
+ try { fs.unlinkSync(tmp); } catch {}
67
+ }
47
68
 
48
- if (!download()) {
49
- console.log(' cloudflared install failed (download error) skipping');
50
- cleanup();
51
- process.exit(0);
52
- }
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
+ }
53
75
 
54
- // Validate downloaded file is not HTML (e.g. 404 page)
55
- try {
56
- const buf = Buffer.alloc(1024);
57
- const fd = fs.openSync(tmp, 'r');
58
- const bytesRead = fs.readSync(fd, buf, 0, 1024, 0);
59
- fs.closeSync(fd);
60
- const head = buf.slice(0, bytesRead).toString('utf8').trim();
61
- if (/^<!doctype\s+html/i.test(head) || /^<html/i.test(head)) {
62
- console.log(' cloudflared install failed (downloaded file is not a binary) — skipping');
76
+ if (!download()) {
77
+ console.log(' cloudflared install failed (download error) — skipping');
63
78
  cleanup();
64
79
  process.exit(0);
65
80
  }
66
- } catch (e) {
67
- console.log(' cloudflared install failed (cannot validate download): ' + e.message);
68
- cleanup();
69
- process.exit(0);
70
- }
71
81
 
72
- function installBin(src, dest) {
82
+ // Validate downloaded file is not HTML (e.g. 404 page)
73
83
  try {
74
- fs.renameSync(src, dest);
75
- } catch (e) {
76
- if (e.code === 'EXDEV') {
77
- fs.copyFileSync(src, dest);
78
- try { fs.unlinkSync(src); } catch {}
79
- } else {
80
- throw e;
84
+ const buf = Buffer.alloc(1024);
85
+ const fd = fs.openSync(tmp, 'r');
86
+ const bytesRead = fs.readSync(fd, buf, 0, 1024, 0);
87
+ fs.closeSync(fd);
88
+ const head = buf.slice(0, bytesRead).toString('utf8').trim();
89
+ if (/^<!doctype\s+html/i.test(head) || /^<html/i.test(head)) {
90
+ console.log(' cloudflared install failed (downloaded file is not a binary) — skipping');
91
+ cleanup();
92
+ process.exit(0);
81
93
  }
94
+ } catch (e) {
95
+ console.log(' cloudflared install failed (cannot validate download): ' + e.message);
96
+ cleanup();
97
+ process.exit(0);
82
98
  }
83
- // Always set executable bit regardless of rename or copy path
84
- try { fs.chmodSync(dest, 0o755); } catch {}
85
- }
86
99
 
87
- try {
88
- if (platform === 'darwin') {
89
- const result = spawnSync('tar', ['xzf', tmp, '-C', tmpDir], { stdio: 'inherit' });
90
- if (result.status !== 0) throw new Error('tar extraction failed');
91
- installBin(path.join(tmpDir, 'cloudflared'), '/usr/local/bin/cloudflared');
92
- try { fs.unlinkSync(tmp); } catch {}
93
- } else if (platform === 'win32') {
94
- const progFiles = process.env.ProgramW6432 || process.env.ProgramFiles || (process.env.SystemRoot + '\\Program Files');
95
- const dest = path.join(progFiles, 'cloudflared', 'cloudflared.exe');
96
- fs.mkdirSync(path.dirname(dest), { recursive: true });
97
- installBin(tmp, dest);
98
- } else {
99
- installBin(tmp, '/usr/local/bin/cloudflared');
100
- }
101
- console.log(' cloudflared installed');
102
- cleanup();
103
- } catch (e) {
104
- // When installed globally via npm, /usr/local/bin may not be writable.
105
- // In that case, download to a local path and suggest manual install.
106
- if (e.code === 'EACCES' || e.code === 'EPERM') {
107
- const localDest = path.join(process.cwd(), 'cloudflared' + (platform === 'win32' ? '.exe' : ''));
100
+ function installBin(src, dest) {
108
101
  try {
109
- installBin(tmp, localDest);
110
- console.log(' cloudflared installed to ' + localDest);
111
- console.log(' Move it to your PATH: sudo mv ' + localDest + ' /usr/local/bin/');
112
- } catch (e2) {
113
- console.log(' cloudflared install failed: ' + e2.message + ' — skipping');
102
+ fs.renameSync(src, dest);
103
+ } catch (e) {
104
+ if (e.code === 'EXDEV') {
105
+ fs.copyFileSync(src, dest);
106
+ try { fs.unlinkSync(src); } catch {}
107
+ } else {
108
+ throw e;
109
+ }
114
110
  }
115
- } else {
116
- console.log(' cloudflared install failed: ' + e.message + ' — skipping');
111
+ try { fs.chmodSync(dest, 0o755); } catch {}
112
+ }
113
+
114
+ 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');
127
+ }
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' : ''));
133
+ 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');
139
+ }
140
+ } else {
141
+ console.log(' cloudflared install failed: ' + e.message + ' — skipping');
142
+ }
143
+ cleanup();
117
144
  }
118
- cleanup();
119
- process.exit(0);
120
145
  }
package/server.js CHANGED
@@ -16,7 +16,17 @@ try {
16
16
  const express = require('express');
17
17
  const http = require('http');
18
18
  const WebSocket = require('ws');
19
- const pty = require('node-pty');
19
+ let pty;
20
+ try {
21
+ pty = require('node-pty');
22
+ } catch (e) {
23
+ console.error('\n Error: node-pty failed to load. Terminal functionality requires native build tools.');
24
+ console.error(' Install build tools and reinstall:');
25
+ console.error(' Linux: sudo apt-get install -y python3 make g++');
26
+ console.error(' macOS: xcode-select --install');
27
+ console.error(' Then reinstall: npm install webtun\n');
28
+ process.exit(1);
29
+ }
20
30
  const multer = require('multer');
21
31
  const fs = require('fs');
22
32
  const path = require('path');