From fd4bee35ebac64473cfe95b79ec2c335529f3213 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe <259325684+Sucukdeluxe@users.noreply.github.com> Date: Sat, 12 Sep 2026 14:26:03 +0200 Subject: [PATCH] fix: complete Doodstream login redirects before session checks --- PROJECT_MEMORY.md | 1 + lib/doodstream-upload.js | 36 +++++++++++++++++++------- tests/doodstream-upload.test.js | 46 +++++++++++++++++++++++++++++++++ 3 files changed, 74 insertions(+), 9 deletions(-) diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 1f97a3e..95e72b2 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -58,6 +58,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. - 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. diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index 4798bb9..2d7b73f 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -72,6 +72,9 @@ class DoodstreamUploader { } async _fetch(url, opts = {}, _redirectCount = 0) { + if (opts.allowedOrigin && new URL(url).origin !== opts.allowedOrigin) { + throw new Error('Doodstream Login: Unerwartetes Weiterleitungsziel'); + } const MAX_REDIRECTS = 10; const headers = { 'User-Agent': USER_AGENT, @@ -98,7 +101,7 @@ class DoodstreamUploader { } catch (err) { if (opts.signal && opts.signal.aborted) throw err; // caller abort: don't retry if (attempt >= 3) throw err; - _debugLog(`_fetch transient (${attempt}/3) ${url}: ${err && err.message}; retry`); + _debugLog(`_fetch transient (${attempt}/3) ${new URL(url).origin}: retry`); await new Promise(r => setTimeout(r, 400 * attempt)); } } @@ -147,22 +150,24 @@ class DoodstreamUploader { const res = await fetch(`${BASE_URL}/?op=login_ajax&${loginData.toString()}`, { method: 'GET', headers, - redirect: 'manual' + redirect: 'manual', + signal: AbortSignal.timeout(20000) }); this._parseCookiesFromHeaders(res.headers); + let loginRedirect = ''; // On successful login, server may redirect (3xx) to dashboard if ([301, 302, 303, 307, 308].includes(res.status)) { try { await res.text(); } catch {} - // Redirect means login succeeded + loginRedirect = res.headers.get('location') || ''; } else { const body = await res.text(); let json; try { json = JSON.parse(body); } catch { json = null; } if (json && ['success', 'redirect'].includes(json.status)) { - // Explicit success response + if (json.status === 'redirect') loginRedirect = json.message || ''; } else if (json && (json.status === 'otp_sent' || (json.message && /otp|verification code/i.test(json.message)))) { // OTP required — signal caller to collect OTP from user const err = new Error(`Doodstream Login: ${json.message || 'OTP erforderlich'}`); @@ -178,7 +183,20 @@ class DoodstreamUploader { } } - // Extract sess_id from the upload page + if (loginRedirect) { + const target = new URL(loginRedirect, BASE_URL); + if (target.origin !== BASE_URL || target.username || target.password) { + throw new Error('Doodstream Login: Unerwartetes Weiterleitungsziel'); + } + const landing = await this._fetch(target.href, { allowedOrigin: BASE_URL }); + const landingHtml = await landing.text(); + const sessId = this._findSessId(landingHtml); + if (landing.status === 200 && sessId) { + this.sessId = sessId; + return; + } + } + await this._extractSessId(); } @@ -186,13 +204,13 @@ class DoodstreamUploader { const res = await this._fetch(BASE_URL + '/?op=upload'); const html = await res.text(); const sessId = this._findSessId(html); - - if (sessId) { + if (res.status === 200 && sessId) { this.sessId = sessId; return; } - - throw new Error('Doodstream: sess_id nicht gefunden nach Login'); + 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})`); } _findSessId(html) { diff --git a/tests/doodstream-upload.test.js b/tests/doodstream-upload.test.js index 7f9d9b9..33a4ba9 100644 --- a/tests/doodstream-upload.test.js +++ b/tests/doodstream-upload.test.js @@ -215,6 +215,52 @@ test('_findSessId accepts the current Vue upload data and URL-safe tokens', () = assert.equal(up._findSessId(``), ''); }); +test('login follows the session-establishing redirect before loading the upload page', async (t) => { + const up = new DoodstreamUploader(); + const requests = []; + t.mock.method(globalThis, 'fetch', async (url, options) => { + const parsed = new URL(url); + const route = parsed.pathname + (parsed.searchParams.get('op') || ''); + requests.push(route); + if (parsed.searchParams.get('op') === 'login_ajax') { + return new Response(JSON.stringify({ status: 'redirect', message: '/finish-login?ticket=TEST' })); + } + if (parsed.pathname === '/finish-login') { + return new Response('', { headers: { 'set-cookie': 'session=TEST; Path=/' } }); + } + if (parsed.searchParams.get('op') === 'upload') { + assert.equal(options.headers.Cookie, 'session=TEST'); + return new Response(''); + } + return new Response(''); + }); + await up.login('user', 'password', '123456'); + assert.deepEqual(requests, ['/', '/login_ajax', '/finish-login', '/upload']); + assert.equal(up.sessId, 'TEST_SESSION'); +}); + +test('login cannot report success when the redirected session is still a guest', async (t) => { + const up = new DoodstreamUploader(); + t.mock.method(globalThis, 'fetch', async url => new Response( + new URL(url).searchParams.get('op') === 'login_ajax' + ? JSON.stringify({ status: 'redirect', message: '/dashboard' }) + : `` + )); + await assert.rejects(up.login('user', 'password'), /guest=true; sessionField=true; cookies=0/); + assert.equal(up.sessId, ''); +}); + +test('login redirect chains cannot forward session cookies to another origin', async (t) => { + const up = new DoodstreamUploader(); + let calls = 0; + t.mock.method(globalThis, 'fetch', async () => { + calls++; + return new Response('', { status: 302, headers: { location: 'https://other.example/' } }); + }); + await assert.rejects(up._fetch('https://doodstream.com/dashboard', { allowedOrigin: 'https://doodstream.com' }), /Weiterleitungsziel/); + assert.equal(calls, 1); +}); + test('getUploadServer: parses the current upload_get_srv response', async () => { const up = new DoodstreamUploader(); up._fetch = async (url) => {