From f890c2fd3e67654fb9ffb7bc0e115901abadb648 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Tue, 22 Sep 2026 05:22:08 +0200 Subject: [PATCH] Fix Vidmoly session verification and preserve upload denial causes --- PROJECT_MEMORY.md | 2 + lib/vidmoly-upload.js | 44 ++++++++++--- renderer/i18n.js | 8 +++ scripts/verify-public-release.mjs | 1 + tests/public-release-verifier.test.js | 2 +- tests/vidmoly-upload.test.js | 93 +++++++++++++++++++++++++++ 6 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 tests/vidmoly-upload.test.js diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 4f9d9b7..41e73a9 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -6,6 +6,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große ## Aktueller Zustand +- Vidmoly nach `2.1.48`, noch nicht veröffentlicht: Anmeldung wird über `/api/auth/me` statt anhand der Upload-Konfiguration verifiziert. Öffentliches Website-JavaScript am 22.09.2026 bestätigt unveränderte Upload-Felder sowie `DISK_FULL`, `UPLOAD_IP_BLACKLIST` und `UPLOAD_DISABLED`; diese Antworten werden jetzt getrennt und ohne Antwortdaten angezeigt. Upload übernimmt zusätzlich das aktuelle Webfeld `tos=1`, weiterhin ohne API-Key. HTTP-Fehler, fehlende Benutzer, OTP-Anforderung und ungültige Serveradressen werden geprüft. Die konkrete Serverantwort des gemeldeten Accounts war in den lokalen Logs nicht vorhanden; erfolgreicher Live-Login/Upload ist noch nicht bestätigt. - Nach `2.1.48` lokal geändert, noch nicht veröffentlicht: Checkboxen in den Hoster-Einstellungen übernehmen beim Mausklick nicht mehr den Fokus-Schatten von Textfeldern. Tastatur-Fokusmarkierung bleibt erhalten; echter isolierter Electron-Test prüft An-/Abhaken, Tabulator, Leertaste und unveränderten Zahlenfeld-Fokus. - Hoster-Dateigrößenlimit wird direkt in GB eingegeben (`0` unbegrenzt, `1 GB = 1024 MB`). Beide Speicherwege rechnen verlustfrei in das bestehende `maxSizeMb`-Format um; bestehende Einstellungen und Backups bleiben kompatibel. Import-Vorprüfung und Upload-Manager überspringen zu große Dateien pro Hoster, lassen die exakte Grenze und andere Hoster zu. Regressionen prüfen 5 GB, einen Byte darüber sowie Dezimalwerte. Vorab übersprungene Jobs melden korrekt 0 Versuche statt der konfigurierten Höchstzahl. @@ -85,6 +86,7 @@ npm audit --omit=dev ## Zuletzt verifiziert +- Vidmoly-Änderung vom 22.09.2026: alle sieben neuen Regressionen und Lint erfolgreich. Hauptlauf: 836 von 837 Tests erfolgreich; bestehender Electron-Checkbox-Fokustest einmal mit Timingfehler, separater Wiederholungslauf der gesamten Datei mit 23 von 23 erfolgreich. Backup-API weiterhin 18 von 18 erfolgreich. Öffentlicher Quellcheck umfasst 164 Dateien. Keine Live-Anmeldung und kein Release durchgeführt. - Checkbox-/GB-Limit-Änderung nach `2.1.48`: 830 Haupttests und 18 Servertests erfolgreich; Lint ohne Fehler. Echter Electron-Fokustest sowie GB-Speicher-, Import- und Upload-Grenztests erfolgreich. Änderungen noch nicht als Update veröffentlicht. - Release `2.1.48`: Titel, Sprachen, Veröffentlichung, jeweils vier Assets und Update-Metadaten beider Plattformen verifiziert. Alle acht Assets erneut heruntergeladen und per Größe/SHA-512 mit lokalen Dateien verglichen. Eingebauter Updater mit Version `2.1.47` erkennt `2.1.48`, übernimmt englische Release Notes und akzeptiert das Forgejo-Manifest. Release-Commit `7a49a19`, annotierter Tag `b8df102` auf beiden Remotes identisch. Anleitung auf dem Desktop aktualisiert. diff --git a/lib/vidmoly-upload.js b/lib/vidmoly-upload.js index cd9b145..3c5f2ca 100644 --- a/lib/vidmoly-upload.js +++ b/lib/vidmoly-upload.js @@ -104,23 +104,30 @@ class VidmolyUploader { }); const body = await res.text(); - if (res.status === 401 || res.status === 403 || /incorrect|invalid|wrong/i.test(body)) { + if (res.status === 401) { throw new Error('Vidmoly Login fehlgeschlagen: Falscher Username oder Passwort'); } if (res.status < 200 || res.status >= 300) { throw new Error(`Vidmoly Login fehlgeschlagen: HTTP ${res.status}`); } - if (!this.cookies.has('vidmoly_session')) { + let loginPayload = null; + try { loginPayload = JSON.parse(body); } catch {} + if (Number(loginPayload?.otp_required) === 1) { + throw new Error('Vidmoly Login fehlgeschlagen: OTP erforderlich'); + } + if (!this.cookies.get('vidmoly_session')) { throw new Error('Vidmoly Login fehlgeschlagen: Keine Session erhalten (vidmoly_session fehlt)'); } - // Probe the upload API so downstream getUploadParams() has a warm path. - const probe = await this._fetch(`${BASE_URL}/api/upload/config`); + const probe = await this._fetch(`${BASE_URL}/api/auth/me`); const probeBody = await probe.text(); let probeJson = null; try { probeJson = JSON.parse(probeBody); } catch {} - if (!probeJson || !probeJson.sess_id || !probeJson.upload_url) { - throw new Error('Vidmoly Login fehlgeschlagen: Session konnte nicht verifiziert werden (API-Probe)'); + if (probe.status < 200 || probe.status >= 300) { + throw new Error(`Vidmoly Login fehlgeschlagen: Sitzungsprüfung HTTP ${probe.status}`); + } + if (!probeJson?.user || typeof probeJson.user !== 'object' || Array.isArray(probeJson.user) || !Object.keys(probeJson.user).length) { + throw new Error('Vidmoly Login fehlgeschlagen: Sitzungsprüfung enthält keinen Benutzer'); } } @@ -133,17 +140,36 @@ class VidmolyUploader { const res = await this._fetch(`${BASE_URL}/api/upload/config`); const body = await res.text(); let payload = null; - try { payload = JSON.parse(body); } catch { + try { payload = JSON.parse(body); } catch {} + const errorCode = String(payload?.message || payload?.statusMessage || ''); + if (/\bDISK_FULL\b/.test(errorCode)) { + throw new Error('Vidmoly: Speicherplatz des Accounts voll (disk full)'); + } + if (/\bUPLOAD_IP_BLACKLIST\b/.test(errorCode)) { + throw new Error('Vidmoly: Upload für diese IP-Adresse gesperrt'); + } + if (/\bUPLOAD_DISABLED\b/.test(errorCode)) { + throw new Error('Vidmoly: Upload für diesen Account deaktiviert'); + } + if (res.status < 200 || res.status >= 300) { + throw new Error(`Vidmoly: Upload-Konfiguration HTTP ${res.status}`); + } + if (!payload) { throw new Error('Vidmoly: /api/upload/config lieferte kein JSON — evtl. nicht eingeloggt?'); } - if (!payload || !payload.sess_id || !payload.upload_url) { + if (typeof payload.sess_id !== 'string' || !payload.sess_id.trim() || typeof payload.upload_url !== 'string' || !payload.upload_url.trim()) { throw new Error('Vidmoly: /api/upload/config unvollständig (sess_id/upload_url fehlt)'); } + let uploadUrl; + try { uploadUrl = new URL(payload.upload_url); } catch {} + if (!uploadUrl || !['http:', 'https:'].includes(uploadUrl.protocol) || uploadUrl.username || uploadUrl.password) { + throw new Error('Vidmoly: Ungültige Upload-Server-Adresse'); + } return { uploadUrl: payload.upload_url, // Fields verified from a real browser POST capture. // to_json=1 forces a JSON response instead of an HTML redirect page. - params: { sess_id: payload.sess_id, to_json: '1', fld_id: '0' }, + params: { sess_id: payload.sess_id, tos: '1', to_json: '1', fld_id: '0' }, fileFieldName: 'file' }; } diff --git a/renderer/i18n.js b/renderer/i18n.js index 4d5bb28..72a3c25 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -685,6 +685,12 @@ ['Vidmoly: /api/upload/config lieferte kein JSON — evtl. nicht eingeloggt?', 'Vidmoly: /api/upload/config did not return JSON — you may not be signed in'], ['Vidmoly: /api/upload/config unvollständig (sess_id/upload_url fehlt)', 'Vidmoly: /api/upload/config is incomplete (sess_id/upload_url is missing)'], ['Vidmoly VM API lieferte kein JSON', 'The Vidmoly VM API did not return JSON'], + ['Vidmoly Login fehlgeschlagen: OTP erforderlich', 'Vidmoly login failed: OTP required'], + ['Vidmoly Login fehlgeschlagen: Sitzungsprüfung enthält keinen Benutzer', 'Vidmoly login failed: Session verification returned no user'], + ['Vidmoly: Speicherplatz des Accounts voll (disk full)', 'Vidmoly: Account storage is full (disk full)'], + ['Vidmoly: Upload für diese IP-Adresse gesperrt', 'Vidmoly: Uploads are blocked for this IP address'], + ['Vidmoly: Upload für diesen Account deaktiviert', 'Vidmoly: Uploads are disabled for this account'], + ['Vidmoly: Ungültige Upload-Server-Adresse', 'Vidmoly: Invalid upload server address'], ['VOE Login: CSRF-Token nicht gefunden', 'VOE login: CSRF token not found'], ['VOE Upload: CSRF-Token nicht gefunden. Bist du eingeloggt?', 'VOE upload: CSRF token not found. Are you signed in?'], ['VOE: Kein Upload-Server erhalten von delivery-node', 'VOE: No upload server was returned by delivery-node'], @@ -818,6 +824,8 @@ [/^API-Antwort war kein JSON (.+)$/, 'The API response was not JSON $1'], [/^Kein Upload-Server erhalten: (.+)$/, 'No upload server was returned: $1'], [/^Upload-Antwort von (.+) war kein JSON (.+)$/, 'The upload response from $1 was not JSON $2'], + [/^Vidmoly Login fehlgeschlagen: Sitzungsprüfung HTTP (.+)$/, 'Vidmoly login failed: Session verification HTTP $1'], + [/^Vidmoly: Upload-Konfiguration HTTP (.+)$/, 'Vidmoly: Upload configuration HTTP $1'], [/^Vidmoly Login fehlgeschlagen: (.+)$/, 'Vidmoly login failed: $1'], [/^Vidmoly Upload abgelehnt: (.+)$/, 'Vidmoly upload rejected: $1'], [/^Vidmoly Upload-Ergebnis: (.+)$/, 'Vidmoly upload result: $1'], diff --git a/scripts/verify-public-release.mjs b/scripts/verify-public-release.mjs index e2a3059..fd64973 100644 --- a/scripts/verify-public-release.mjs +++ b/scripts/verify-public-release.mjs @@ -164,6 +164,7 @@ const sourceFiles = [ 'tests/upload-confirmation.test.js', 'tests/upload-diagnostics.test.js', 'tests/upload-manager.test.js', + 'tests/vidmoly-upload.test.js', 'tests/session-report.test.js', 'tests/validate-credentials.test.js', 'tests/webhook-notify.test.js' diff --git a/tests/public-release-verifier.test.js b/tests/public-release-verifier.test.js index a6cc6db..045334d 100644 --- a/tests/public-release-verifier.test.js +++ b/tests/public-release-verifier.test.js @@ -45,7 +45,7 @@ test('public release verifier accepts only the exact source manifest and target assert.equal(baseline.status, 0, baseline.stderr); assert.equal( baseline.stdout, - `public-release-source-ok files=163 denied-paths=0 internal-terms=0 version=${currentVersion} scripts=8 build-files=7 layout=exact screenshot=deferred\n` + `public-release-source-ok files=164 denied-paths=0 internal-terms=0 version=${currentVersion} scripts=8 build-files=7 layout=exact screenshot=deferred\n` ); fs.writeFileSync(path.join(stage, 'tests', 'unexpected.json'), '{}'); diff --git a/tests/vidmoly-upload.test.js b/tests/vidmoly-upload.test.js new file mode 100644 index 0000000..424df07 --- /dev/null +++ b/tests/vidmoly-upload.test.js @@ -0,0 +1,93 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const VidmolyUploader = require('../lib/vidmoly-upload'); + +function response(status, payload) { + return { status, text: async () => typeof payload === 'string' ? payload : JSON.stringify(payload) }; +} + +function loginFixture({ loginStatus = 200, login = {}, session = 'test-session', probeStatus = 200, probe = { user: { usr_id: 1 } } } = {}) { + const uploader = new VidmolyUploader(); + const calls = []; + uploader._fetch = async (url, options) => { + calls.push({ url, options }); + if (url.endsWith('/api/auth/login')) { + if (session !== null) uploader.cookies.set('vidmoly_session', session); + return response(loginStatus, login); + } + if (url.endsWith('/api/auth/me')) return response(probeStatus, probe); + assert.equal(url, 'https://vidmoly.me'); + return response(200, ''); + }; + return { uploader, calls }; +} + +test('Vidmoly verifies the website session independently of upload availability', async () => { + const { uploader, calls } = loginFixture({ probe: { user: { usr_id: 1 }, upload: { allowed: false, reason: 'disk_full' } } }); + await uploader.login('test-user', 'test-password'); + assert.deepEqual(calls.map(call => call.url), [ + 'https://vidmoly.me', 'https://vidmoly.me/api/auth/login', 'https://vidmoly.me/api/auth/me' + ]); + assert.deepEqual(JSON.parse(calls[1].options.body), { login: 'test-user', password: 'test-password' }); +}); + +test('Vidmoly does not interpret arbitrary successful response text as a password error', async () => { + await loginFixture({ login: { login: 'wrong-example' } }).uploader.login('test-user', 'test-password'); +}); + +test('Vidmoly stops after an OTP challenge without probing or repeating login', async () => { + const { uploader, calls } = loginFixture({ login: { otp_required: 1 }, session: null }); + await assert.rejects(uploader.login('test-user', 'test-password'), /OTP erforderlich/); + assert.equal(calls.length, 2); +}); + +test('Vidmoly rejects missing sessions, invalid users and failed session probes', async () => { + for (const options of [{ session: null }, { session: '' }, { probe: {} }, { probe: { user: {} } }, { probe: { user: [] } }, { probe: 'Login' }, { probeStatus: 401 }, { probeStatus: 503 }]) { + await assert.rejects(loginFixture(options).uploader.login('test-user', 'test-password'), /Vidmoly Login fehlgeschlagen/); + } + await assert.rejects(loginFixture({ loginStatus: 401 }).uploader.login('test-user', 'test-password'), /Falscher Username/); + await assert.rejects(loginFixture({ loginStatus: 403 }).uploader.login('test-user', 'test-password'), /HTTP 403/); +}); + +test('Vidmoly preserves upload denial causes without exposing response data', async () => { + for (const [code, expected] of [ + ['DISK_FULL', /disk full/], + ['UPLOAD_IP_BLACKLIST', /IP-Adresse gesperrt/], + ['UPLOAD_DISABLED', /Account deaktiviert/] + ]) { + for (const status of [200, 403]) { + const uploader = new VidmolyUploader(); + uploader._fetch = async () => response(status, { message: `${code}: private-data` }); + await assert.rejects(uploader.getUploadParams(), error => expected.test(error.message) && !error.message.includes('private-data')); + } + } +}); + +test('Vidmoly validates config responses before streaming a file', async () => { + for (const [status, payload, expected] of [ + [401, {}, /HTTP 401/], + [503, 'Unavailable', /HTTP 503/], + [200, 'Login', /kein JSON/], + [200, {}, /unvollständig/], + [200, { sess_id: {}, upload_url: 'https://upload.example/' }, /unvollständig/], + [200, { sess_id: 'test', upload_url: 'file:///private' }, /Ungültige/], + [200, { sess_id: 'test', upload_url: 'https://user:password@upload.example/' }, /Ungültige/] + ]) { + const uploader = new VidmolyUploader(); + uploader._fetch = async () => response(status, payload); + await assert.rejects(uploader.getUploadParams(), expected); + } +}); + +test('Vidmoly uses the current website upload fields without an API key', async () => { + const uploader = new VidmolyUploader(); + uploader._fetch = async url => { + assert.equal(url, 'https://vidmoly.me/api/upload/config'); + return response(200, { sess_id: 'test-session', upload_url: 'https://upload.example/upload' }); + }; + assert.deepEqual(await uploader.getUploadParams(), { + uploadUrl: 'https://upload.example/upload', + params: { sess_id: 'test-session', tos: '1', to_json: '1', fld_id: '0' }, + fileFieldName: 'file' + }); +});