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 +39 -0
- package/bin/webtun.js +74 -2
- package/package.json +3 -9
- package/postinstall.js +113 -88
- package/server.js +11 -1
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).
|
|
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.
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
82
|
+
// Validate downloaded file is not HTML (e.g. 404 page)
|
|
73
83
|
try {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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');
|