Fix Vidmoly session verification and preserve upload denial causes
CI / verify (push) Waiting to run
CI / verify (push) Waiting to run
This commit is contained in:
@@ -6,6 +6,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große
|
|||||||
|
|
||||||
## Aktueller Zustand
|
## 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.
|
- 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.
|
- 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
|
## 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.
|
- 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.
|
- 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.
|
||||||
|
|||||||
+35
-9
@@ -104,23 +104,30 @@ class VidmolyUploader {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const body = await res.text();
|
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');
|
throw new Error('Vidmoly Login fehlgeschlagen: Falscher Username oder Passwort');
|
||||||
}
|
}
|
||||||
if (res.status < 200 || res.status >= 300) {
|
if (res.status < 200 || res.status >= 300) {
|
||||||
throw new Error(`Vidmoly Login fehlgeschlagen: HTTP ${res.status}`);
|
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)');
|
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/auth/me`);
|
||||||
const probe = await this._fetch(`${BASE_URL}/api/upload/config`);
|
|
||||||
const probeBody = await probe.text();
|
const probeBody = await probe.text();
|
||||||
let probeJson = null;
|
let probeJson = null;
|
||||||
try { probeJson = JSON.parse(probeBody); } catch {}
|
try { probeJson = JSON.parse(probeBody); } catch {}
|
||||||
if (!probeJson || !probeJson.sess_id || !probeJson.upload_url) {
|
if (probe.status < 200 || probe.status >= 300) {
|
||||||
throw new Error('Vidmoly Login fehlgeschlagen: Session konnte nicht verifiziert werden (API-Probe)');
|
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 res = await this._fetch(`${BASE_URL}/api/upload/config`);
|
||||||
const body = await res.text();
|
const body = await res.text();
|
||||||
let payload = null;
|
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?');
|
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)');
|
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 {
|
return {
|
||||||
uploadUrl: payload.upload_url,
|
uploadUrl: payload.upload_url,
|
||||||
// Fields verified from a real browser POST capture.
|
// Fields verified from a real browser POST capture.
|
||||||
// to_json=1 forces a JSON response instead of an HTML redirect page.
|
// 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'
|
fileFieldName: 'file'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 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: /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 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 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 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'],
|
['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'],
|
[/^API-Antwort war kein JSON (.+)$/, 'The API response was not JSON $1'],
|
||||||
[/^Kein Upload-Server erhalten: (.+)$/, 'No upload server was returned: $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'],
|
[/^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 Login fehlgeschlagen: (.+)$/, 'Vidmoly login failed: $1'],
|
||||||
[/^Vidmoly Upload abgelehnt: (.+)$/, 'Vidmoly upload rejected: $1'],
|
[/^Vidmoly Upload abgelehnt: (.+)$/, 'Vidmoly upload rejected: $1'],
|
||||||
[/^Vidmoly Upload-Ergebnis: (.+)$/, 'Vidmoly upload result: $1'],
|
[/^Vidmoly Upload-Ergebnis: (.+)$/, 'Vidmoly upload result: $1'],
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ const sourceFiles = [
|
|||||||
'tests/upload-confirmation.test.js',
|
'tests/upload-confirmation.test.js',
|
||||||
'tests/upload-diagnostics.test.js',
|
'tests/upload-diagnostics.test.js',
|
||||||
'tests/upload-manager.test.js',
|
'tests/upload-manager.test.js',
|
||||||
|
'tests/vidmoly-upload.test.js',
|
||||||
'tests/session-report.test.js',
|
'tests/session-report.test.js',
|
||||||
'tests/validate-credentials.test.js',
|
'tests/validate-credentials.test.js',
|
||||||
'tests/webhook-notify.test.js'
|
'tests/webhook-notify.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.status, 0, baseline.stderr);
|
||||||
assert.equal(
|
assert.equal(
|
||||||
baseline.stdout,
|
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'), '{}');
|
fs.writeFileSync(path.join(stage, 'tests', 'unexpected.json'), '{}');
|
||||||
|
|||||||
@@ -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: '<html>Login</html>' }, { 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, '<html>Unavailable</html>', /HTTP 503/],
|
||||||
|
[200, '<html>Login</html>', /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'
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user