webtun 1.5.4 → 1.5.6
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 +10 -4
- package/package.json +1 -1
- package/public/index.html +740 -122
- package/server.js +27 -9
package/server.js
CHANGED
|
@@ -77,13 +77,18 @@ const MIME_MAP = {
|
|
|
77
77
|
'.go':'text/x-go','.rs':'text/x-rust','.php':'text/x-php','.pl':'text/x-perl',
|
|
78
78
|
'.sql':'application/sql','.graphql':'application/graphql',
|
|
79
79
|
'.yaml':'text/yaml','.yml':'text/yaml','.toml':'application/toml','.ini':'text/plain',
|
|
80
|
-
'.env':'text/plain','.lock':'text/plain',
|
|
80
|
+
'.env':'text/plain','.lock':'text/plain','.epub':'application/epub+zip',
|
|
81
81
|
};
|
|
82
82
|
function mimeLookup(filePath) {
|
|
83
83
|
const ext = path.extname(filePath).toLowerCase();
|
|
84
84
|
return MIME_MAP[ext] || 'application/octet-stream';
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Binary extensions — preview not supported in editor (client has same list). Keep in sync.
|
|
88
|
+
const BINARY_EXTS = new Set([
|
|
89
|
+
'3dm','3ds','3g2','3gp','7z','a','aac','adp','afdesign','afphoto','afpub','ai','aif','aiff','alz','ape','apk','appimage','ar','arj','asf','au','avi','bak','baml','bh','bin','bk','bmp','btif','bz2','bzip2','cab','caf','cgm','class','cmx','cpio','cr2','cr3','cur','dat','dcm','deb','dex','djvu','dll','dmg','dng','doc','docm','docx','dot','dotm','dra','ds_store','dsk','dts','dtshd','dvb','dwg','dxf','ecelp4800','ecelp7470','ecelp9600','egg','eol','eot','epub','exe','f4v','fbs','fh','fla','flac','flatpak','fli','flv','fpx','fst','fvt','g3','gh','gif','graffle','gz','gzip','h261','h263','h264','icns','ico','ief','img','ipa','iso','jar','jpeg','jpg','jpgv','jpm','jxr','key','ktx','lha','lib','lvp','lz','lzh','lzma','lzo','m3u','m4a','m4v','mar','mdi','mht','mid','midi','mj2','mka','mkv','mmr','mng','mobi','mov','movie','mp3','mp4','mp4a','mpeg','mpg','mpga','mxu','nef','npx','numbers','nupkg','o','odp','ods','odt','oga','ogg','ogv','otf','ott','pages','pbm','pcx','pdb','pdf','pea','pgm','pic','png','pnm','pot','potm','potx','ppa','ppam','ppm','pps','ppsm','ppsx','ppt','pptm','pptx','psd','pya','pyc','pyo','pyv','qt','rar','ras','raw','resources','rgb','rip','rlc','rmf','rmvb','rpm','rtf','rz','s3m','s7z','scpt','sgi','shar','snap','sil','sketch','slk','smv','snk','so','stl','suo','sub','swf','tar','tbz','tbz2','tga','tgz','thmx','tif','tiff','tlz','ttc','ttf','txz','udf','uvh','uvi','uvm','uvp','uvs','uvu','viv','vob','war','wav','wax','wbmp','wdp','weba','webm','webp','whl','wim','wm','wma','wmv','wmx','woff','woff2','wrm','wvx','xbm','xif','xla','xlam','xls','xlsb','xlsm','xlsx','xlt','xltm','xltx','xm','xmind','xpi','xpm','xwd','xz','z','zip','zipx'
|
|
90
|
+
]);
|
|
91
|
+
|
|
87
92
|
// In-memory rate limiter factory
|
|
88
93
|
// NOTE: Behind cloudflared tunnel every remote IP appears as 127.0.0.1 (tunnel collapses to loopback).
|
|
89
94
|
// We use req.ip (Express respects app.set('trust proxy')) so limiter correctly respects trust proxy config.
|
|
@@ -132,7 +137,7 @@ const SHELL = (os.platform() === 'win32' && !process.env.WEBTUN_SHELL)
|
|
|
132
137
|
? 'powershell.exe'
|
|
133
138
|
: (process.env.SHELL || (os.platform() === 'win32' ? 'powershell.exe' : (fs.existsSync('/bin/bash') ? '/bin/bash' : 'sh')));
|
|
134
139
|
const HOST = process.env.HOST || '0.0.0.0';
|
|
135
|
-
const ALLOW_FULL_FS = process.env.ALLOW_FULL_FS
|
|
140
|
+
const ALLOW_FULL_FS = process.env.ALLOW_FULL_FS !== 'false';
|
|
136
141
|
const WORKSPACE_ROOT = (() => {
|
|
137
142
|
let ws = process.env.WORKSPACE_ROOT ? path.resolve(process.env.WORKSPACE_ROOT) : os.homedir();
|
|
138
143
|
if (!ws || ws.trim() === '') ws = os.homedir() || process.cwd();
|
|
@@ -169,7 +174,7 @@ app.use((req, res, next) => {
|
|
|
169
174
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
170
175
|
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
171
176
|
res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
172
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'; connect-src 'self' https://*.trycloudflare.com wss:; script-src 'self' https://cdn.jsdelivr.net
|
|
177
|
+
res.setHeader('Content-Security-Policy', "default-src 'self'; connect-src 'self' https://*.trycloudflare.com wss: blob:; script-src 'self' https://cdn.jsdelivr.net blob:; style-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; font-src 'self' data: https://fonts.gstatic.com https://fonts.googleapis.com; img-src 'self' data: blob:; frame-src 'self' blob:; child-src 'self' blob:; worker-src 'self' blob: https://cdn.jsdelivr.net;");
|
|
173
178
|
next();
|
|
174
179
|
});
|
|
175
180
|
|
|
@@ -613,7 +618,7 @@ app.get('/api/files', checkPin, async (req, res) => {
|
|
|
613
618
|
const full = path.join(dir, e.name);
|
|
614
619
|
const st = await safeStat(full);
|
|
615
620
|
files.push({
|
|
616
|
-
name: e.name, path: full, isDir: e.isDirectory(),
|
|
621
|
+
name: e.name, path: full, isDir: st ? st.isDirectory() : e.isDirectory(),
|
|
617
622
|
isSymlink: e.isSymbolicLink(), size: st ? st.size : 0,
|
|
618
623
|
modified: st ? st.mtime : null, ext: path.extname(e.name).toLowerCase()
|
|
619
624
|
});
|
|
@@ -633,11 +638,14 @@ app.get('/api/files', checkPin, async (req, res) => {
|
|
|
633
638
|
entries.map(async e => {
|
|
634
639
|
const full = path.join(dir, e.name);
|
|
635
640
|
const st = await safeStat(full);
|
|
641
|
+
const isSymlink = e.isSymbolicLink();
|
|
642
|
+
// Use stat result for isDir so symlink→dir is navigable; Dirent.isDirectory() is false for symlink
|
|
643
|
+
const isDir = st ? st.isDirectory() : e.isDirectory();
|
|
636
644
|
return {
|
|
637
645
|
name: e.name,
|
|
638
646
|
path: full,
|
|
639
|
-
isDir
|
|
640
|
-
isSymlink
|
|
647
|
+
isDir,
|
|
648
|
+
isSymlink,
|
|
641
649
|
size: st ? st.size : 0,
|
|
642
650
|
modified: st ? st.mtime : null,
|
|
643
651
|
ext: path.extname(e.name).toLowerCase()
|
|
@@ -648,8 +656,10 @@ app.get('/api/files', checkPin, async (req, res) => {
|
|
|
648
656
|
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1;
|
|
649
657
|
return a.name.localeCompare(b.name);
|
|
650
658
|
});
|
|
651
|
-
|
|
652
|
-
|
|
659
|
+
let parent = path.dirname(dir);
|
|
660
|
+
if (parent === dir) parent = null;
|
|
661
|
+
else if (!ALLOW_FULL_FS && !pathContained(WORKSPACE_ROOT, parent)) parent = null;
|
|
662
|
+
res.json({ path: dir, parent, files });
|
|
653
663
|
} catch (e) {
|
|
654
664
|
const status = e.status || 500;
|
|
655
665
|
res.status(status).json({ error: e.message });
|
|
@@ -901,7 +911,15 @@ app.get('/api/files/read', checkPin, async (req, res) => {
|
|
|
901
911
|
const st = await fsPromises.stat(p);
|
|
902
912
|
if (st.isDirectory()) return res.status(400).json({ error: 'Cannot read a directory' });
|
|
903
913
|
if (st.size > 10 * 1024 * 1024) return res.status(413).json({ error: 'File too large (max 10MB) - use download' });
|
|
904
|
-
const
|
|
914
|
+
const ext = path.extname(p).toLowerCase().slice(1);
|
|
915
|
+
if (ext && BINARY_EXTS.has(ext)) {
|
|
916
|
+
return res.status(415).json({ error: 'Preview not supported for binary files - use download', isBinary: true });
|
|
917
|
+
}
|
|
918
|
+
const buf = await fsPromises.readFile(p);
|
|
919
|
+
if (buf.includes(0)) {
|
|
920
|
+
return res.status(415).json({ error: 'Preview not supported for binary files - use download', isBinary: true });
|
|
921
|
+
}
|
|
922
|
+
const content = buf.toString('utf8');
|
|
905
923
|
res.json({ content, length: st.size });
|
|
906
924
|
} catch (e) {
|
|
907
925
|
const status = e.status || 500;
|