diff options
| author | Agando <roduccomm@gmail.com> | 2026-10-02 21:21:15 +0200 |
|---|---|---|
| committer | Agando <roduccomm@gmail.com> | 2026-10-02 21:21:15 +0200 |
| commit | 9a81c96b5a3941898f99665335ad679ff2105666 (patch) | |
| tree | 75c8dbf1b059dee8ff67e37a7cc3e9cb4dfd298d /scripts/test-webdav-paths.mjs | |
| parent | baedca20a4c50845525a7490cd8bf513c6785d69 (diff) | |
newOS4 webdav path fix + WebX11 resilience package (2026-10-02)
Root cause fixes (mindtech incident):
- webdav.js: _normalizeRemotePath strips full URLs/space prefixes, applied
to all request entry points + _absoluteUrl for MOVE/COPY Destinations
- _ensureDir: __list__ existence fast-path (kills MKCOL 409 storms)
- dashboard-files.js: updateFilesJson parses __list__ autoindex (no more
'#' entries), ensureSpaceExists only re-inits on genuine 404
- 20-test regression suite: scripts/test-webdav-paths.mjs
Resilience:
- dav-auth-check.py: server-side WebDAV write auth microservice (nginx
auth_request on 8904) — anonymous/wrong-token writes now 401
- fail2ban-telegram.py: ban alerts + auto-unban for known space users
- backup-spaces.sh: daily hardlink snapshots, 14-day retention
- space-health-check.py: junk dirs / myfiles.json / version drift / session
sanity → Telegram alerts
- dashboard-files.js: visible 503 error panel with Retry + auto-retry
- v4.6.16: main.html + module-loader LATEST_VER aligned (was drifted)
Diffstat (limited to 'scripts/test-webdav-paths.mjs')
| -rw-r--r-- | scripts/test-webdav-paths.mjs | 136 |
1 files changed, 136 insertions, 0 deletions
diff --git a/scripts/test-webdav-paths.mjs b/scripts/test-webdav-paths.mjs new file mode 100644 index 0000000..a8340c0 --- /dev/null +++ b/scripts/test-webdav-paths.mjs @@ -0,0 +1,136 @@ +// v4.6.16 regression test: WebDAV remote-path normalization. +// Reproduces the mindtech incident: AI passed full URLs / absolute +// '/spaces/<user>/' paths which got blindly joined onto the config URL, +// creating literal 'https:/' and nested 'spaces/<user>/' junk dirs. +// Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4) + +// Run: node scripts/test-webdav-paths.mjs (repo root = spaces/newos4) + +import { readFileSync } from 'fs'; + +const src = readFileSync(new URL('../modules/ext/webdav.js', import.meta.url), 'utf8'); + +class TerminalStub { + print() {} +} + +// Mirrors ModuleManager.createModuleClass(): extract the first top-level class. +const cleanCode = src.replace(/^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*\s*/, ''); +const classMatch = cleanCode.match(/class\s+(\w+)\s+(extends\s+\S+)?\s*\{/); +if (!classMatch) throw new Error('no module class found in webdav.js'); +const WebDAVModule = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto', + 'return ' + cleanCode + '\nreturn ' + classMatch[1])( + { location: { pathname: '/spaces/mindtech/newos4/main.html' } }, + { getItem: () => null, setItem: () => {} }, + async () => { throw new Error('network disabled in test'); }, + function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; }, + URL, undefined); + +function makeMod() { + const m = new WebDAVModule(new TerminalStub()); + m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' }; + return m; +} + +let pass = 0, fail = 0; +function check(name, cond, got) { + if (cond) { pass++; console.log(' ok - ' + name); } + else { fail++; console.log(' FAIL - ' + name + (got !== undefined ? ' got: ' + JSON.stringify(got) : '')); } +} + +// --- _normalizeRemotePath --- +{ + const m = makeMod(); + const cases = [ + // [input, expected] + [null, '/'], + ['', '/'], + ['file.txt', '/file.txt'], + ['/file.txt', '/file.txt'], + // the mindtech incident shapes: + ['https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden/index-traditional.html', + '/thaimassage-kiefersfelden/index-traditional.html'], + ['/spaces/mindtech/thaimassage-kiefersfelden/index-professional.html', + '/thaimassage-kiefersfelden/index-professional.html'], + ['https://webx11.duckdns.org/spaces/mindtech', '/'], + ['spaces/mindtech/skills/navigating-the-terminal.json', '/skills/navigating-the-terminal.json'], + // relative + traversal + dupes: + ['./a//b/../c.txt', '/a/c.txt'], + ['/a/b/../../d.txt', '/d.txt'], + ['/skills/', '/skills'], + ['backups', '/backups'], + // foreign space stays intact (only own space prefix is stripped): + ['/spaces/otheruser/file.txt', '/spaces/otheruser/file.txt'], + ['https://evil.example.com/spaces/mindtech/x?query=1#frag', '/x'], + ]; + for (const [input, expected] of cases) { + const got = m._normalizeRemotePath(input); + check(`normalize(${JSON.stringify(input)}) => ${expected}`, got === expected, got); + } +} + +// --- _absoluteUrl (MOVE/COPY Destination) --- +{ + const m = makeMod(); + const got = m._absoluteUrl('https://webx11.duckdns.org/spaces/mindtech/a/b.html'); + check('_absoluteUrl strips to full absolute URI', + got === 'https://webx11.duckdns.org/spaces/mindtech/a/b.html', got); + const got2 = m._absoluteUrl('c.html'); + check('_absoluteUrl builds from config base', + got2 === 'https://webx11.duckdns.org/spaces/mindtech/c.html', got2); +} + +// --- _ensureDir normalizes before MKCOL walk --- +{ + const m = makeMod(); + const mkcols = []; + m._webdavRequest = async (method, path) => { + if (method === 'MKCOL') mkcols.push(path); + return { ok: true, status: 201 }; + }; + m._listDir = async () => { throw new Error('ENOENT'); }; + await m._ensureDir('https://webx11.duckdns.org/spaces/mindtech/thaimassage-kiefersfelden'); + check('_ensureDir MKCOLs normalized path (no https:/ junk)', + mkcols.length === 1 && mkcols[0] === '/thaimassage-kiefersfelden', mkcols); + + // Existing dir fast-path: __list__ probe succeeds -> no MKCOL at all + const m2 = makeMod(); + const mkcols2 = []; + m2._listDir = async (p) => { if (p === '/skills') return []; throw new Error('ENOENT'); }; + m2._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols2.push(path); return { ok: true, status: 201 }; }; + await m2._ensureDir('/skills'); + check('_ensureDir skips MKCOL when dir already exists', mkcols2.length === 0, mkcols2); + + // Partial existing: /backups exists, /backups/2026 does not + const m3 = makeMod(); + const mkcols3 = []; + m3._listDir = async (p) => { if (p === '/backups') return []; throw new Error('ENOENT'); }; + m3._webdavRequest = async (method, path) => { if (method === 'MKCOL') mkcols3.push(path); return { ok: true, status: 201 }; }; + await m3._ensureDir('/backups/2026'); + check('_ensureDir MKCOLs only missing tail', mkcols3.length === 1 && mkcols3[0] === '/backups/2026', mkcols3); +} + +// --- PUT via _webdavRequest hits normalized URL --- +{ + // The class closes over the injected fetch param — rebuild with a + // recording fetch so we can assert the request URL. + let requested = null; + const recordFetch = async (url) => { requested = url; return { ok: true, status: 201 }; }; + const WebDAVModule2 = new Function('window', 'localStorage', 'fetch', 'XMLHttpRequest', 'URL', 'crypto', + 'return ' + cleanCode + '\nreturn ' + classMatch[1])( + { location: { pathname: '/spaces/mindtech/newos4/main.html' } }, + { getItem: () => null, setItem: () => {} }, + recordFetch, + function () { this.open = () => {}; this.setRequestHeader = () => {}; this.send = () => {}; }, + URL, undefined); + const m = new WebDAVModule2(new TerminalStub()); + m.config = { url: 'https://webx11.duckdns.org/spaces/mindtech/', username: 'mindtech', password: 'tok' }; + m._resolvePassword = async () => {}; + m._getAuthHeader = () => ({}); + await m._webdavRequest('PUT', 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', { body: 'x' }); + check('PUT goes to normalized URL', + requested === 'https://webx11.duckdns.org/spaces/mindtech/site/index.html', requested); +} + +console.log(`\n${pass} passed, ${fail} failed`); +process.exit(fail ? 1 : 0);
\ No newline at end of file |