From 0e4a9613fbb2960be779d87812fa749b839663a9 Mon Sep 17 00:00:00 2001
From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com>
Date: Sat, 12 Sep 2026 14:38:10 +0200
Subject: [PATCH] fix: preserve explicit Doodstream web login and reuse OTP
sessions
---
PROJECT_MEMORY.md | 8 ++--
lib/account-auth.js | 25 ++++++++++--
lib/doodstream-upload.js | 70 ++++++++++++++++++++++++++++-----
lib/upload-manager.js | 22 +++--------
main.js | 4 +-
renderer/i18n.js | 1 +
tests/account-auth.test.js | 18 ++++++++-
tests/doodstream-upload.test.js | 52 +++++++++++++++++++++++-
tests/upload-manager.test.js | 22 +++++++++++
9 files changed, 188 insertions(+), 34 deletions(-)
diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md
index 95e72b2..e00ee2d 100644
--- a/PROJECT_MEMORY.md
+++ b/PROJECT_MEMORY.md
@@ -18,7 +18,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große
- Die jüngste Automatik schützt erfolgreiche Uploads mit einem atomar gespeicherten Abschlussnachweis aus vollständigem Pfad, Hoster, Dateigröße und Änderungszeit.
- Schlägt dieser Nachweis fehl, bleibt die Queue erhalten und der Fehler wird als lokale Persistenzstörung behandelt, damit kein stiller Doppel-Upload entsteht.
- DoodStream-OTP-Prüfungen verwenden dieselbe Cookie-Sitzung weiter, fassen identische oder parallele Checks zusammen und fordern einen neuen Code nur nach einer ausdrücklichen Aktion mit mindestens 60 Sekunden Abstand an.
-- DoodStream-Accounts mit API-Key werden auch beim Health-Check über die API geprüft und lösen keinen Web-OTP aus.
+- DoodStream respektiert die ausdrückliche Auswahl `authType=login`: Weblogin und Webupload werden nicht durch einen zusätzlich gespeicherten API-Key übersteuert. API-Accounts und ältere Accounts ohne ausdrückliche Login-Auswahl behalten den API-Weg.
- Sammelchecks melden jedes Account-Ergebnis einzeln an den Renderer, sodass fertige Karten sofort grün, rot oder als OTP-pflichtig erscheinen, während die übrigen Accounts weiter geprüft werden.
- Neue Online-Backups unterstützen `24 Stunden`, `3 Tage`, `7 Tage` (Standard), `31 Tage` und `Unbegrenzt`. Endliche Schlüssel werden lokal aus dem verschlüsselten Schlüsselbund entfernt und serverseitig ab Ablauf nicht mehr wiederhergestellt; der Dienst räumt abgelaufene Datensätze bei Zugriff oder der nächsten Speicherung auf.
- Vorhandene Online-Backups und alte Upload-Payloads ohne Ablaufangabe bleiben zur Abwärtskompatibilität unbegrenzt gültig.
@@ -27,6 +27,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große
- Upload-Status-Badges und ihre Textlabels sind nicht markierbar; kopierbare Fehlerdetails, Logs und Eingabefelder behalten ihre Textauswahl.
- VOE-Fehler mit der Meldung `Maximum storage space of the account used up.` gelten als temporärer Accountfehler. Die Retry-Schleife bricht auch nach einem bereits erfolgten Account-Wechsel sofort ab und setzt die Fallback-Kette Account für Account fort, bis ein Upload gelingt oder kein weiterer Account verfügbar ist.
- Der DoodStream-Weblogin folgt dem aktuellen Browservertrag über `GET /?op=login_ajax`, behandelt `otp_sent` und `redirect` ausdrücklich und übernimmt `sess_id` auch aus den aktuellen Vue-Daten mit URL-sicheren Sonderzeichen. Die Upload-Server-Ermittlung verwendet `/?op=upload_get_srv` und versteht dessen `server.srv_url`-/`server.disk_id`-Antwort.
+- Eine bestätigte DoodStream-Dashboard-Sitzung benötigt beim Account-Check kein Upload-Sessionfeld. Uploads übernehmen unabhängige Kopien der bestätigten Cookie-Sitzung aus dem OTP-Koordinator. Ein fehlender Upload-Server ist vom Login getrennt; bei Web-Accounts findet keine automatische API-Key-Ableitung für Uploads statt.
- Version `2.1.44` ist als GitHub- und Forgejo-Release veröffentlicht; Backup-API `2.0.4` blieb bei dieser reinen Veröffentlichung der Desktopanwendung unverändert aktiv.
- Der eingebaute Updater liest Releases und Binärdateien von Forgejo; GitHub liefert ergänzend die öffentlichen Release Notes. Ein Release ist deshalb erst vollständig, wenn die vier Assets auch im Forgejo-Release vorhanden sind.
- Forgejo bewahrt Leerzeichen in Asset-Namen, GitHub normalisiert sie zu Punkten. Das Forgejo-`latest.yml` und der Release-Plan verwenden Namen wie `Multi-Hoster-Upload Setup 2.1.44.exe`; das GitHub-Manifest muss auf den dort tatsächlich veröffentlichten Punktnamen zeigen.
@@ -58,7 +59,7 @@ npm audit --omit=dev
## Offene nächste Schritte
-- DoodStream: Der echte OTP-Test nach dem ersten Kompatibilitätsfix meldete weiterhin fehlendes `sess_id`. JSON- und HTTP-Weiterleitungen werden nun vor der Sessionprüfung aufgerufen, einschließlich der dort gesetzten Cookies. 35 gezielte Tests und Lint sind erfolgreich; ein erneuter echter OTP-Login steht aus. Die konkrete Ursache der Nutzersitzung ist noch nicht abschließend bestätigt. Fehlende Sessions liefern HTTP-Status, Gastseiten-/Sessionfeld-Erkennung und Cookie-Anzahl ohne Cookie-Werte oder Zugangsdaten.
+- DoodStream: Am 12.09.2026 wurde das authentifizierte Dashboard ohne `sess_id` live bestätigt. Der alte Upload-Aufruf lieferte eine andere Seite ohne Upload-Felder. Ein zwischenzeitlich getesteter API-Ausweichweg bestätigte zwar den Account, wurde auf Nutzerwunsch wieder entfernt; dessen Uploadversuch scheiterte serverseitig mit `No servers available for uploads`. Der aktuelle Web-Upload muss noch live auf Serververfügbarkeit und erfolgreichen Dateitransfer geprüft werden. Die lokale Seitendiagnose protokolliert ausschließlich Strukturmerkmale ohne Formularwerte, OTP oder Cookie-Werte.
- Keine offenen Schritte für Release `v2.1.44`; Rollback-Ziel ist Anwendungsversion `2.1.43`.
- Bei Bedarf einen Arbeitsweg ohne `&` im absoluten Pfad verwenden oder die npm-Aufrufe weiterhin direkt ausführen.
@@ -67,10 +68,11 @@ npm audit --omit=dev
Stand: 12.09.2026
- Lint: erfolgreich, 0 Warnungen und 0 Fehler.
-- Haupttests: 808 erfolgreich, 0 fehlgeschlagen.
+- Haupttests: vollständiger Lauf erfolgreich, 0 fehlgeschlagen.
- Backup-API-Tests: 17 erfolgreich, 0 fehlgeschlagen.
- Der Regressionstest für die VOE-Fallback-Kette bestätigt bei deaktivierter normaler Rotation genau einen Versuch auf jedem vollen Account und anschließend den erfolgreichen Wechsel auf den vierten Account.
- Der öffentliche DoodStream-Webablauf wurde am 12.09.2026 direkt gegen die Startseite und deren aktuelle Browser-Skripte geprüft. Regressionstests bilden den neuen GET-Login, `otp_sent`, `redirect`, Vue-Sessiontokens mit `_`/`-` und die aktuelle `upload_get_srv`-Antwort nach.
+- Der lokale Web-Account-Check um 14:35:27 bestätigte das authentifizierte Dashboard ohne Upload-Sessionfeld. Regressionen prüfen zusätzlich explizite Web-Auswahl trotz gespeichertem API-Key, Wiederverwendung der OTP-Sitzung, getrennte Cookie-Kopien für parallele Uploads und Web-Serverausfälle. Der Upload-Aufruf verwendet nachweislich weder API-Ableitung noch einen zweiten Login.
- Das Support-Bundle vom 07.09.2026 bestätigt als Ursache der gemeldeten Datei: Wechsel vom Primäraccount auf `Fallback #1`, dort vier unnötige Versuche, anschließend `skip-account-pause` und Abbruch mit `override-same-as-current` statt Weiterschaltung.
- Der vollständige opt-in UI-Smoke bestätigte zusätzlich, dass Upload-Status-Badges und deren Labels nicht markierbar sind; die 16 bekannten themenfremden Abweichungen blieben unverändert.
- Produktionsabhängigkeiten: `npm audit --omit=dev` meldet 0 Schwachstellen.
diff --git a/lib/account-auth.js b/lib/account-auth.js
index c82c549..42fe97a 100644
--- a/lib/account-auth.js
+++ b/lib/account-auth.js
@@ -20,6 +20,10 @@ const { createHash } = require('node:crypto');
function selectUploadAuth(hoster, account) {
if (!account || typeof account !== 'object') return {};
+ if (account.authType === 'login' && account.username && account.password) {
+ return { username: account.username, password: account.password };
+ }
+
if (hoster === 'doodstream.com' && account.apiKey) {
return { apiKey: account.apiKey };
}
@@ -72,6 +76,7 @@ function createDoodstreamOtpCoordinator(options = {}) {
const key = credentialKey(username, password);
const existing = activeState(key);
if (existing?.inFlight) return existing.inFlight;
+ if (existing?.ready && input.requestNewChallenge !== true) return existing.result;
if (otp && !existing?.pending) {
return {
status: 'otp_required',
@@ -87,8 +92,11 @@ function createDoodstreamOtpCoordinator(options = {}) {
const operation = (async () => {
try {
await uploader.login(username, password, otp || undefined);
- if (states.get(key)?.operationId === operationId) states.delete(key);
- return { status: 'ok', message: 'Login ok, Upload-Seite bereit' };
+ const result = { status: 'ok', message: 'Login erfolgreich' };
+ if (states.get(key)?.operationId === operationId) {
+ storeState(key, { operationId, uploader, ready: true, expiresAt: now() + challengeTtlMs, result, inFlight: null });
+ }
+ return result;
} catch (error) {
if (error?.otpRequired === true) {
const result = { status: 'otp_required', message: error.message || 'OTP erforderlich' };
@@ -137,7 +145,18 @@ function createDoodstreamOtpCoordinator(options = {}) {
return operation;
}
- return { check };
+ async function acquire(input) {
+ const result = await check(input);
+ const session = activeState(credentialKey(input.username, input.password));
+ if (result.status !== 'ok' || !session?.ready) {
+ const error = new Error(result.message || 'OTP erforderlich');
+ error.otpRequired = result.status === 'otp_required';
+ throw error;
+ }
+ return session.uploader.cloneSession();
+ }
+
+ return { check, acquire };
}
module.exports = { createDoodstreamOtpCoordinator, selectUploadAuth };
diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js
index 2d7b73f..572bed0 100644
--- a/lib/doodstream-upload.js
+++ b/lib/doodstream-upload.js
@@ -30,8 +30,8 @@ function _doodstreamLogPath() {
let _debugVerbose = false;
function setDebugVerbose(v) { _debugVerbose = !!v; }
-function _debugLog(msg) {
- if (!_debugVerbose) return;
+function _debugLog(msg, force = false) {
+ if (!_debugVerbose && !force) return;
try {
const logPath = _doodstreamLogPath();
maybeRotateLogFile(logPath, _DOODSTREAM_LOG_MAX_BYTES, _DOODSTREAM_LOG_MAX_BACKUPS);
@@ -53,6 +53,20 @@ class DoodstreamUploader {
.join('; ');
}
+ cloneSession() {
+ const uploader = new DoodstreamUploader();
+ uploader.cookies = new Map(this.cookies);
+ uploader.sessId = this.sessId;
+ return uploader;
+ }
+
+ _isAuthenticatedPage(html) {
+ return /href=["']\/settings["']/i.test(html)
+ && /href=["']\/videos["']/i.test(html)
+ && /href=["'][^"']*(?:op=logout|\/logout)["']/i.test(html)
+ && !/]*\bname=["'](?:login|password|loginotp)["']/i.test(html);
+ }
+
_parseCookiesFromHeaders(headers) {
let setCookies;
if (typeof headers.getSetCookie === 'function') {
@@ -175,8 +189,9 @@ class DoodstreamUploader {
throw err;
} else if (json && json.status === 'fail') {
throw new Error(`Doodstream Login: ${json.message || 'Login fehlgeschlagen'}`);
- } else if (body.includes('Dashboard')) {
- // Got dashboard HTML directly — login worked
+ } else if (res.status === 200 && this._isAuthenticatedPage(body)) {
+ this.sessId = this._findSessId(body);
+ return;
} else {
const msg = (json && json.message) || 'Login fehlgeschlagen';
throw new Error(`Doodstream Login: ${msg}`);
@@ -190,8 +205,9 @@ class DoodstreamUploader {
}
const landing = await this._fetch(target.href, { allowedOrigin: BASE_URL });
const landingHtml = await landing.text();
+ this._diagnoseSessionPage('landing', landing, landingHtml);
const sessId = this._findSessId(landingHtml);
- if (landing.status === 200 && sessId) {
+ if (landing.status === 200 && (sessId || this._isAuthenticatedPage(landingHtml))) {
this.sessId = sessId;
return;
}
@@ -203,6 +219,7 @@ class DoodstreamUploader {
async _extractSessId() {
const res = await this._fetch(BASE_URL + '/?op=upload');
const html = await res.text();
+ this._diagnoseSessionPage('upload', res, html);
const sessId = this._findSessId(html);
if (res.status === 200 && sessId) {
this.sessId = sessId;
@@ -210,7 +227,8 @@ class DoodstreamUploader {
}
this.sessId = '';
const guest = /utype\s*:\s*['"]anon['"]/.test(html);
- throw new Error(`Doodstream: sess_id nicht gefunden nach Login (HTTP ${res.status}; guest=${guest}; sessionField=${/sess_id/.test(html)}; cookies=${this.cookies.size})`);
+ _debugLog(`session-missing HTTP=${res.status} guest=${guest} cookies=${this.cookies.size}`, true);
+ throw new Error('Doodstream: sess_id nicht gefunden nach Login');
}
_findSessId(html) {
@@ -225,6 +243,32 @@ class DoodstreamUploader {
return match ? match[2].trim() : '';
}
+ _diagnoseSessionPage(stage, response, html) {
+ const routes = new Set();
+ for (const match of html.matchAll(/\bhref=["']([^"']+)["']/gi)) {
+ try {
+ const url = new URL(match[1].replace(/&/g, '&'), BASE_URL);
+ if (url.origin !== BASE_URL) continue;
+ const op = url.searchParams.get('op') || '';
+ if (/^[a-z_]+$/i.test(op)) routes.add(`op=${op}`);
+ if (/^\/[a-z_/-]*$/i.test(url.pathname)) routes.add(url.pathname);
+ } catch {}
+ }
+ const fields = [...html.matchAll(/<(?:input|textarea)\b[^>]*\bname=["']([a-z_]+)["']/gi)].map(match => match[1]);
+ const components = [...html.matchAll(/<([a-z]+-[a-z-]+)\b/g)].map(match => match[1]);
+ const scripts = [...html.matchAll(/