webtun 1.5.3 → 1.5.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 +25 -5
- package/bin/webtun.js +12 -0
- package/electron/main.js +176 -0
- package/electron/preload.js +7 -0
- package/package.json +43 -4
- package/postinstall.js +38 -20
- package/public/commands.js +1 -1
- package/public/index.html +610 -214
- package/public/manifest.json +1 -1
- package/public/sw.js +16 -3
- package/server.js +651 -166
package/README.md
CHANGED
|
@@ -65,19 +65,20 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
65
65
|
|
|
66
66
|
### Terminal
|
|
67
67
|
- **Real shell sessions** — node-pty backed, full bash/zsh/PowerShell support
|
|
68
|
-
- **Multi-tab** — drag to reorder, side-by-side sessions
|
|
69
|
-
- **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop)
|
|
68
|
+
- **Multi-tab** — drag to reorder, side-by-side sessions, new-tab button next to last tab
|
|
69
|
+
- **Bracketed paste** — Ctrl+V works in TUI apps (vim, nano, htop) — 60KB chunked
|
|
70
70
|
- **Command history** — keystroke-based capture, strips ANSI/control sequences
|
|
71
71
|
- **Session persistence** — tmux or in-memory PTY; tabs survive page reload (all platforms)
|
|
72
72
|
- **Keyboard Shortcuts** — reference dialog in the overflow menu (Ctrl+P/T/W/B/F, etc.)
|
|
73
73
|
|
|
74
74
|
### File Explorer
|
|
75
|
-
- Browse, upload, download, rename, delete files
|
|
75
|
+
- Browse, upload, download, rename, delete files — **full filesystem access** (`ALLOW_FULL_FS=true` by default)
|
|
76
76
|
- **Multi-select** — batch delete, download, zip, cut, copy
|
|
77
77
|
- **Zip/Extract** — right-click any file or folder
|
|
78
78
|
- **Image viewer** — preview inline (png, jpg, gif, svg, webp)
|
|
79
79
|
- **Conflict resolution** — replace, merge, keep both, skip
|
|
80
|
-
- **Breadcrumb navigation** — tappable
|
|
80
|
+
- **Breadcrumb navigation** — tappable segments, folder icon + path one line, `2px` tight spacing, header/breadcrumb unified `30px` bar
|
|
81
|
+
- **Editor** — CodeMirror with autosave draft, `F11` fullscreen (covers terminal area only, never the file explorer), `horizontal/vertical` split toggle, markdown/HTML preview
|
|
81
82
|
|
|
82
83
|
### Mobile
|
|
83
84
|
- **Touch gestures** — swipe to close tabs, pull-to-refresh
|
|
@@ -96,7 +97,7 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
96
97
|
- CPU, Memory, Disk, Uptime — real-time with progress bars
|
|
97
98
|
- GPU detection (nvidia-smi, system_profiler, WMI)
|
|
98
99
|
- Multi-GPU support with VRAM, utilization, temperature
|
|
99
|
-
- Top processes sorted by CPU
|
|
100
|
+
- Top processes sorted by CPU — **Kill** button per row (`POST /api/system/kill`)
|
|
100
101
|
|
|
101
102
|
### Desktop (Electron)
|
|
102
103
|
- Cross-platform app for Linux, Windows, macOS
|
|
@@ -133,6 +134,7 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
133
134
|
| `PIN` | Auth PIN (empty = no auth) | none |
|
|
134
135
|
| `SHELL` | Shell to use | platform default |
|
|
135
136
|
| `WORKSPACE_ROOT` | File explorer root | `~` |
|
|
137
|
+
| `ALLOW_FULL_FS` | Allow browsing outside `WORKSPACE_ROOT` (full FS) | `true` |
|
|
136
138
|
| `TRUST_PROXY` | Trust `X-Forwarded-For` from first proxy | `false` |
|
|
137
139
|
| `WEBTUN_SHELL` | Override shell on Windows | PowerShell |
|
|
138
140
|
|
|
@@ -175,6 +177,24 @@ git clone https://github.com/unn-Known1/webtun.git && cd webtun && ./setup.sh &&
|
|
|
175
177
|
|
|
176
178
|
## Changelog
|
|
177
179
|
|
|
180
|
+
### v1.5.5
|
|
181
|
+
- Explorer: full filesystem access by default (`ALLOW_FULL_FS=true` unless `ALLOW_FULL_FS=false`), symlink dirs now navigable (`stat` `isDirectory`), fixed silent `403` bounce on `..` (now `toast` + error div), `parent` hidden at workspace root, `history`/`breadcrumb` desync fixed (deferred to success), stale closures on reused file items fixed (`dataset` live), drag overlay stuck fixed (`relatedTarget`/`dragend`/`blur`), relative `path-input` now `joinPath(currentPath)`, `rename` dialog no longer closes on error
|
|
182
|
+
- Settings: drawer flicker fixed (`#main:isolation`, `#sidebar`/`#settings-panel` `will-change`/`backface-visibility`, `#file-list-wrap` `content-visibility` removed, `inert`/`focusTrap` deferred via `rAF` + `preventScroll`, `fileWatcher` skips when drawer/overlay open)
|
|
183
|
+
- Editor: `F11` fullscreen now `absolute` inside terminal area only (`#editor-split-area` `relative`), never covers file explorer/sidebar or header, removed `requestFullscreen` browser overlay, `CodeMirror` height `100%`
|
|
184
|
+
- Terminal: removed `Shift+F11` fullscreen button and `#terminals.fullscreen`/`#content.terminal-fullscreen` per request
|
|
185
|
+
- UI: `#conn-status` `margin-left:6px` before dot
|
|
186
|
+
|
|
187
|
+
### v1.5.4
|
|
188
|
+
- Security hardening: path sandbox `ALLOW_FULL_FS` (default `~`), symlink containment, null-byte/array guards, `rename` traversal block, `delete` uses `lstat`, batch caps 100, zip 1GB/unzip bomb guards, `download` header sanitization, `image` no-store, `write` 10MB cap, tunnel SSRF (localhost-only) + URL validation
|
|
189
|
+
- Auth & transport: `checkPin` array guard, length-leak free `constantTimeEqual`, rate limiter uses `req.ip` with `trust proxy` + Map cap 10k, `--pin` arg consumption fix, WS `cols/rows` clamp 2-500, Origin check + `constantTimeEqual` token, `sessionId` length 1-64, per-IP `clipboard`, `history` max 1k & `cmdhist.json` 0600 atomic writes, `ptySessions` TTL 30m + cap 100, `TMUX_PREFIX=wt-webtun-`
|
|
190
|
+
- Build: predictable `tmp` via `mkdtemp`, tar-slip `tzf` validation, heredoc `printf` no-expansion, `.env` parser first-`=` fix, `pkill $SCRIPT_DIR` + `kill -0` race, `electron` `parseInt PORT` + `autostart` filter, `ws ^8.17.1`
|
|
191
|
+
- UI/UX: touch 32-36px, `aria-pressed`/`aria-expanded`/`aria-modal`/`inert` (siblings), focus-trap fix, `confirmDialog` queue, `skip-link`, `pan-y` + `contain`, `visualViewport` Samsung+scale, `pull-to-refresh`, `safe-area`, `cmd-lib` trap, `tiles min(300px,100%)`, `upload 6px+cancel`, `paste 60KB`, `WebGL<4`, `iframe allow-scripts`, `autosave draft 2s` + `jsx:true`
|
|
192
|
+
- Explorer: `new-tab` next to last tab (sticky), `sidebar-header`/`breadcrumb` unified `30px` `3px 6px gap3`, `file-item 2→1px` `breadcrumb 1px sep`, `sidebar-footer/bookmarks` compact, folder icon + path one line `flex gap2`
|
|
193
|
+
- Editor: `F11` fullscreen (`fixed inset0`) + `horizontal/vertical` split toggle stays usable, `Esc` to exit, `requestFullscreen` fallback, `closeEditor` clears
|
|
194
|
+
- Terminal: `Shift+F11` fullscreen (`#content.terminal-fullscreen` + `#terminals.fullscreen`), header button, `fitTerm` refit
|
|
195
|
+
- System Stats: **Kill** `POST /api/system/kill` per-row button (refuse `1/self/cloudflared`)
|
|
196
|
+
- PWA: `sw.js` no immediate `skipWaiting`, `token`/`/ws` bypass, stale-while-revalidate, `manifest` `?newTerm=1` + `titlebar-area` CSS
|
|
197
|
+
|
|
178
198
|
### v1.5.3
|
|
179
199
|
- Terminal session persistence without tmux (in-memory PTY on Windows + Linux)
|
|
180
200
|
- Session IDs always generated; tabs now survive page reload on all platforms
|
package/bin/webtun.js
CHANGED
|
@@ -49,14 +49,26 @@ function parseArgs(argv) {
|
|
|
49
49
|
process.exit(0);
|
|
50
50
|
}
|
|
51
51
|
if (arg === '--port' || arg === '-p') {
|
|
52
|
+
if (!argv[i+1] || argv[i+1].startsWith('-')) {
|
|
53
|
+
console.error('Error: --port requires a value');
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
52
56
|
opts.port = parseInt(argv[++i], 10);
|
|
53
57
|
if (isNaN(opts.port)) {
|
|
54
58
|
console.error('Error: --port requires a numeric value');
|
|
55
59
|
process.exit(1);
|
|
56
60
|
}
|
|
57
61
|
} else if (arg === '--host' || arg === '-h') {
|
|
62
|
+
if (!argv[i+1] || argv[i+1].startsWith('-')) {
|
|
63
|
+
console.error('Error: --host requires a value');
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
58
66
|
opts.host = argv[++i];
|
|
59
67
|
} else if (arg === '--pin') {
|
|
68
|
+
if (!argv[i+1] || argv[i+1].startsWith('-')) {
|
|
69
|
+
console.error('Error: --pin requires a value');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
60
72
|
process.env.PIN = argv[++i] || '';
|
|
61
73
|
} else if (arg === '--tunnel' || arg === '-t') {
|
|
62
74
|
opts.tunnel = true;
|
package/electron/main.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
const { app, BrowserWindow, Menu, dialog, ipcMain } = require('electron');
|
|
2
|
+
const { fork } = require('child_process');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
|
|
7
|
+
let mainWindow;
|
|
8
|
+
let serverProcess;
|
|
9
|
+
const PORT = (() => {
|
|
10
|
+
const p = parseInt(process.env.PORT, 10);
|
|
11
|
+
return Number.isFinite(p) && p > 0 && p <= 65535 ? p : 3000;
|
|
12
|
+
})();
|
|
13
|
+
const PIN = process.env.PIN || '';
|
|
14
|
+
if (PIN && typeof PIN !== 'string') {
|
|
15
|
+
console.warn('Invalid PIN type — expected string');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveNodeModules() {
|
|
19
|
+
// Packaged app: resources/node_modules or app.asar.unpacked/node_modules
|
|
20
|
+
if (app.isPackaged) {
|
|
21
|
+
const candidates = [
|
|
22
|
+
path.join(process.resourcesPath, 'node_modules'),
|
|
23
|
+
path.join(process.resourcesPath, 'app.asar.unpacked', 'node_modules'),
|
|
24
|
+
path.join(path.dirname(app.getAppPath()), 'node_modules'),
|
|
25
|
+
];
|
|
26
|
+
for (const c of candidates) {
|
|
27
|
+
if (fs.existsSync(c)) return c;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return path.join(__dirname, '..', 'node_modules');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function startServer() {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const serverPath = path.join(__dirname, '..', 'server.js');
|
|
36
|
+
const nodeModules = resolveNodeModules();
|
|
37
|
+
const env = {
|
|
38
|
+
...process.env,
|
|
39
|
+
PORT: String(PORT),
|
|
40
|
+
HOST: '127.0.0.1',
|
|
41
|
+
};
|
|
42
|
+
// Validate PIN exists and is a non-empty string before passing to server
|
|
43
|
+
if (PIN && typeof PIN === 'string' && PIN.trim().length > 0) {
|
|
44
|
+
env.PIN = PIN.trim();
|
|
45
|
+
} else if (PIN) {
|
|
46
|
+
console.warn('Invalid PIN — starting without authentication');
|
|
47
|
+
delete env.PIN;
|
|
48
|
+
}
|
|
49
|
+
// Ensure forked server can resolve deps when packaged
|
|
50
|
+
env.NODE_PATH = [nodeModules, env.NODE_PATH].filter(Boolean).join(path.delimiter);
|
|
51
|
+
|
|
52
|
+
serverProcess = fork(serverPath, [], {
|
|
53
|
+
env,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
serverProcess.stdout.on('data', d => console.log('[server]', d.toString().trim()));
|
|
58
|
+
serverProcess.stderr.on('data', d => console.error('[server]', d.toString().trim()));
|
|
59
|
+
|
|
60
|
+
let settled = false;
|
|
61
|
+
const fail = (msg) => {
|
|
62
|
+
if (settled) return;
|
|
63
|
+
settled = true;
|
|
64
|
+
reject(new Error(msg));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
serverProcess.on('exit', code => {
|
|
68
|
+
if (code !== 0) {
|
|
69
|
+
console.error(`Server exited with code ${code}`);
|
|
70
|
+
fail(`Server exited with code ${code}`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
serverProcess.on('error', err => fail(err.message));
|
|
74
|
+
|
|
75
|
+
const deadline = Date.now() + 30000;
|
|
76
|
+
const check = () => {
|
|
77
|
+
if (settled) return;
|
|
78
|
+
if (Date.now() > deadline) return fail('Server start timed out');
|
|
79
|
+
http.get(`http://127.0.0.1:${PORT}/api/auth/required`, res => {
|
|
80
|
+
if (res.statusCode === 200) {
|
|
81
|
+
settled = true;
|
|
82
|
+
resolve();
|
|
83
|
+
} else setTimeout(check, 200);
|
|
84
|
+
}).on('error', () => setTimeout(check, 200));
|
|
85
|
+
};
|
|
86
|
+
setTimeout(check, 500);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function createWindow() {
|
|
91
|
+
mainWindow = new BrowserWindow({
|
|
92
|
+
width: 1100,
|
|
93
|
+
height: 700,
|
|
94
|
+
minWidth: 600,
|
|
95
|
+
minHeight: 400,
|
|
96
|
+
title: 'WebTun',
|
|
97
|
+
icon: path.join(__dirname, '..', 'public', 'icon-512.png'),
|
|
98
|
+
webPreferences: {
|
|
99
|
+
nodeIntegration: false,
|
|
100
|
+
contextIsolation: true,
|
|
101
|
+
preload: path.join(__dirname, 'preload.js')
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const menu = Menu.buildFromTemplate([
|
|
106
|
+
{
|
|
107
|
+
label: 'WebTun',
|
|
108
|
+
submenu: [
|
|
109
|
+
{ label: 'About WebTun', role: 'about' },
|
|
110
|
+
{ type: 'separator' },
|
|
111
|
+
{ label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: () => app.quit() }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
label: 'Edit',
|
|
116
|
+
submenu: [
|
|
117
|
+
{ role: 'undo' }, { role: 'redo' }, { type: 'separator' },
|
|
118
|
+
{ role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }
|
|
119
|
+
]
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
label: 'View',
|
|
123
|
+
submenu: [
|
|
124
|
+
{ role: 'reload' }, { role: 'forceReload' },
|
|
125
|
+
{ type: 'separator' },
|
|
126
|
+
{ role: 'togglefullscreen' },
|
|
127
|
+
{ type: 'separator' },
|
|
128
|
+
{ role: 'toggleDevTools' }
|
|
129
|
+
]
|
|
130
|
+
}
|
|
131
|
+
]);
|
|
132
|
+
Menu.setApplicationMenu(menu);
|
|
133
|
+
|
|
134
|
+
mainWindow.loadURL(`http://127.0.0.1:${PORT}`);
|
|
135
|
+
|
|
136
|
+
mainWindow.on('closed', () => { mainWindow = null; });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
app.whenReady().then(async () => {
|
|
140
|
+
try {
|
|
141
|
+
await startServer();
|
|
142
|
+
createWindow();
|
|
143
|
+
} catch (e) {
|
|
144
|
+
dialog.showErrorBox('WebTun Error', `Failed to start server:\n${e.message}`);
|
|
145
|
+
app.quit();
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
app.on('window-all-closed', () => {
|
|
150
|
+
if (process.platform !== 'darwin') app.quit();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
app.on('before-quit', () => {
|
|
154
|
+
if (serverProcess) {
|
|
155
|
+
try { serverProcess.kill('SIGTERM'); } catch {}
|
|
156
|
+
serverProcess = null;
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
app.on('activate', () => {
|
|
161
|
+
if (mainWindow === null) createWindow();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ── Auto-start on login ──────────────────────────────────────
|
|
165
|
+
ipcMain.handle('get-autostart', () => {
|
|
166
|
+
return app.getLoginItemSettings().openAtLogin;
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
ipcMain.handle('set-autostart', (_event, enabled) => {
|
|
170
|
+
app.setLoginItemSettings({
|
|
171
|
+
openAtLogin: enabled,
|
|
172
|
+
path: process.execPath,
|
|
173
|
+
args: process.argv.slice(1).filter(a => !a.startsWith('--'))
|
|
174
|
+
});
|
|
175
|
+
return app.getLoginItemSettings().openAtLogin;
|
|
176
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
const { contextBridge, ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
contextBridge.exposeInMainWorld('electronAPI', {
|
|
4
|
+
isElectron: true,
|
|
5
|
+
getAutostart: () => ipcRenderer.invoke('get-autostart'),
|
|
6
|
+
setAutostart: (enabled) => ipcRenderer.invoke('set-autostart', enabled)
|
|
7
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webtun",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Self-hosted web terminal with Cloudflare Tunnel, file explorer, and PWA support",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Gaurang Patel",
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
"bin/",
|
|
16
16
|
"server.js",
|
|
17
17
|
"public/",
|
|
18
|
-
"postinstall.js"
|
|
18
|
+
"postinstall.js",
|
|
19
|
+
"electron/"
|
|
19
20
|
],
|
|
20
21
|
"engines": {
|
|
21
22
|
"node": ">=18.0.0"
|
|
@@ -106,9 +107,47 @@
|
|
|
106
107
|
"nodemon": "^3.1.14"
|
|
107
108
|
},
|
|
108
109
|
"overrides": {
|
|
109
|
-
"
|
|
110
|
+
"array-flatten": "npm:@socketregistry/array-flatten@^1",
|
|
111
|
+
"define-properties": "npm:@socketregistry/define-properties@^1",
|
|
112
|
+
"es-define-property": "npm:@socketregistry/es-define-property@^1",
|
|
113
|
+
"es-set-tostringtag": "npm:@socketregistry/es-set-tostringtag@^1",
|
|
114
|
+
"form-data": "4.0.6",
|
|
115
|
+
"function-bind": "npm:@socketregistry/function-bind@^1",
|
|
116
|
+
"globalthis": "npm:@socketregistry/globalthis@^1",
|
|
117
|
+
"gopd": "npm:@socketregistry/gopd@^1",
|
|
118
|
+
"has-property-descriptors": "npm:@socketregistry/has-property-descriptors@^1",
|
|
119
|
+
"has-symbols": "npm:@socketregistry/has-symbols@^1",
|
|
120
|
+
"has-tostringtag": "npm:@socketregistry/has-tostringtag@^1",
|
|
121
|
+
"hasown": "npm:@socketregistry/hasown@^1",
|
|
122
|
+
"isarray": "npm:@socketregistry/isarray@^1",
|
|
123
|
+
"object-keys": "npm:@socketregistry/object-keys@^1",
|
|
124
|
+
"safe-buffer": "npm:@socketregistry/safe-buffer@^1",
|
|
125
|
+
"safer-buffer": "npm:@socketregistry/safer-buffer@^1",
|
|
126
|
+
"side-channel": "npm:@socketregistry/side-channel@^1",
|
|
127
|
+
"typedarray": "npm:@socketregistry/typedarray@^1",
|
|
128
|
+
"ws": "^8.21.0"
|
|
129
|
+
},
|
|
130
|
+
"resolutions": {
|
|
131
|
+
"array-flatten": "npm:@socketregistry/array-flatten@^1",
|
|
132
|
+
"define-properties": "npm:@socketregistry/define-properties@^1",
|
|
133
|
+
"es-define-property": "npm:@socketregistry/es-define-property@^1",
|
|
134
|
+
"es-set-tostringtag": "npm:@socketregistry/es-set-tostringtag@^1",
|
|
135
|
+
"function-bind": "npm:@socketregistry/function-bind@^1",
|
|
136
|
+
"globalthis": "npm:@socketregistry/globalthis@^1",
|
|
137
|
+
"gopd": "npm:@socketregistry/gopd@^1",
|
|
138
|
+
"has-property-descriptors": "npm:@socketregistry/has-property-descriptors@^1",
|
|
139
|
+
"has-symbols": "npm:@socketregistry/has-symbols@^1",
|
|
140
|
+
"has-tostringtag": "npm:@socketregistry/has-tostringtag@^1",
|
|
141
|
+
"hasown": "npm:@socketregistry/hasown@^1",
|
|
142
|
+
"isarray": "npm:@socketregistry/isarray@^1",
|
|
143
|
+
"object-keys": "npm:@socketregistry/object-keys@^1",
|
|
144
|
+
"safe-buffer": "npm:@socketregistry/safe-buffer@^1",
|
|
145
|
+
"safer-buffer": "npm:@socketregistry/safer-buffer@^1",
|
|
146
|
+
"side-channel": "npm:@socketregistry/side-channel@^1",
|
|
147
|
+
"typedarray": "npm:@socketregistry/typedarray@^1",
|
|
148
|
+
"ws": "^8.21.0"
|
|
110
149
|
},
|
|
111
150
|
"allowScripts": {
|
|
112
|
-
"node-pty
|
|
151
|
+
"node-pty": true
|
|
113
152
|
}
|
|
114
153
|
}
|
package/postinstall.js
CHANGED
|
@@ -17,12 +17,21 @@ function rebuildNodePty() {
|
|
|
17
17
|
|
|
18
18
|
console.log(' rebuilding node-pty...');
|
|
19
19
|
try {
|
|
20
|
-
spawnSync('npm', ['rebuild', 'node-pty'], {
|
|
20
|
+
const r = spawnSync('npm', ['rebuild', 'node-pty'], {
|
|
21
21
|
cwd: __dirname,
|
|
22
|
-
stdio: '
|
|
23
|
-
timeout: 120000
|
|
24
|
-
shell: os.platform() === 'win32'
|
|
22
|
+
stdio: 'pipe',
|
|
23
|
+
timeout: 120000
|
|
25
24
|
});
|
|
25
|
+
if (r.status !== 0) {
|
|
26
|
+
const out = (r.stderr ? r.stderr.toString() : '') + (r.stdout ? r.stdout.toString() : '');
|
|
27
|
+
if (out.includes('allow-scripts') || out.toLowerCase().includes('not allowed')) {
|
|
28
|
+
console.log(' npm blocked build scripts (allow-scripts). To fix:');
|
|
29
|
+
console.log(' npm install -g --allow-scripts=webtun,node-pty webtun');
|
|
30
|
+
console.log(' Or: npm config set allow-scripts=webtun,node-pty --location=user');
|
|
31
|
+
console.log(' Then: npm install -g webtun');
|
|
32
|
+
}
|
|
33
|
+
throw new Error(out.trim() || 'rebuild failed with status ' + r.status);
|
|
34
|
+
}
|
|
26
35
|
// Verify it worked
|
|
27
36
|
delete require.cache[require.resolve('node-pty')];
|
|
28
37
|
require('node-pty');
|
|
@@ -34,6 +43,9 @@ function rebuildNodePty() {
|
|
|
34
43
|
console.log(' macOS: xcode-select --install');
|
|
35
44
|
console.log(' Windows: install Visual Studio Build Tools with "Desktop development with C++"');
|
|
36
45
|
console.log(' https://visualstudio.microsoft.com/visual-cpp-build-tools/');
|
|
46
|
+
console.log(' If npm v10+ blocks scripts, allow them:');
|
|
47
|
+
console.log(' npm config set allow-scripts=webtun,node-pty --location=user');
|
|
48
|
+
console.log(' npm install --allow-scripts=webtun,node-pty webtun');
|
|
37
49
|
}
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -89,11 +101,13 @@ if (!file) process.exit(0);
|
|
|
89
101
|
console.log(' installing cloudflared...');
|
|
90
102
|
|
|
91
103
|
const url = CF_RELEASES + '/' + file;
|
|
92
|
-
const tmpDir = os.tmpdir();
|
|
93
|
-
const tmp = path.join(tmpDir, 'cloudflared
|
|
104
|
+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webtun-'));
|
|
105
|
+
const tmp = path.join(tmpDir, 'cloudflared' + (platform === 'win32' ? '.exe' : ''));
|
|
94
106
|
|
|
95
107
|
function cleanup() {
|
|
96
108
|
try { fs.unlinkSync(tmp); } catch {}
|
|
109
|
+
try { fs.rmdirSync(tmpDir); } catch {}
|
|
110
|
+
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
|
|
97
111
|
}
|
|
98
112
|
|
|
99
113
|
function downloadFile(downloadUrl, dest) {
|
|
@@ -121,22 +135,14 @@ function downloadFile(downloadUrl, dest) {
|
|
|
121
135
|
});
|
|
122
136
|
}
|
|
123
137
|
|
|
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
138
|
async function main() {
|
|
132
139
|
try {
|
|
133
140
|
await downloadFile(url, tmp);
|
|
134
141
|
} catch (e) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
142
|
+
console.log(' cloudflared install failed (download error) — skipping: ' + e.message);
|
|
143
|
+
console.log(' (Node https download failed; ensure network access to github.com)');
|
|
144
|
+
cleanup();
|
|
145
|
+
return;
|
|
140
146
|
}
|
|
141
147
|
|
|
142
148
|
// Validate downloaded file is not HTML (e.g. 404 page)
|
|
@@ -183,8 +189,20 @@ async function main() {
|
|
|
183
189
|
|
|
184
190
|
function extractDarwinIfNeeded() {
|
|
185
191
|
if (platform !== 'darwin') return tmp;
|
|
186
|
-
|
|
187
|
-
|
|
192
|
+
// Validate tar entries to prevent tar slip (filter .. and absolute paths)
|
|
193
|
+
const list = spawnSync('tar', ['tzf', tmp], { encoding: 'utf8', timeout: 10000 });
|
|
194
|
+
if (list.stdout) {
|
|
195
|
+
const entries = list.stdout.split('\n').map(s => s.trim()).filter(Boolean);
|
|
196
|
+
for (const entry of entries) {
|
|
197
|
+
if (entry.includes('..') || path.isAbsolute(entry) || entry.startsWith('/')) {
|
|
198
|
+
throw new Error('tar slip detected: invalid entry ' + entry);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} else if (list.status !== 0) {
|
|
202
|
+
throw new Error('tar list failed: ' + (list.stderr ? list.stderr.toString().trim() : 'unknown error'));
|
|
203
|
+
}
|
|
204
|
+
const result = spawnSync('tar', ['xzf', tmp, '-C', tmpDir], { stdio: 'pipe', timeout: 30000 });
|
|
205
|
+
if (result.status !== 0) throw new Error('tar extraction failed: ' + (result.stderr ? result.stderr.toString().trim() : 'unknown'));
|
|
188
206
|
try { fs.unlinkSync(tmp); } catch {}
|
|
189
207
|
return path.join(tmpDir, 'cloudflared');
|
|
190
208
|
}
|
package/public/commands.js
CHANGED
|
@@ -2,5 +2,5 @@ window.DEFAULT_CMDS = [
|
|
|
2
2
|
{ name: 'Install OpenCode AI', cmd: 'npm install -g opencode-ai', cat: 'AI Tools' },
|
|
3
3
|
{ name: 'Install 9Router', cmd: 'npm install -g 9router', cat: 'AI Tools' },
|
|
4
4
|
{ name: 'GitHub TUI', cmd: 'npx github-tui', cat: 'Tools' },
|
|
5
|
-
{ name: 'Colab Setup', cmd: 'curl -fsSL https://raw.githubusercontent.com/unn-Known1/unn-Known1/main/colab_setup.sh | bash', cat: 'Setup' },
|
|
5
|
+
{ name: 'Colab Setup', cmd: 'curl -fsSL https://raw.githubusercontent.com/unn-Known1/unn-Known1/main/colab_setup.sh | bash', cat: 'Setup', requiresConfirm: true },
|
|
6
6
|
];
|