Files
Multi-Hoster-Upload/lib/account-auth.js
T
Sucukdeluxe 40ce83d413
CI / verify (push) Canceled after 0s
fix: prevent repeated Doodstream OTP requests
Coalesce concurrent and repeated Doodstream login checks by credential identity, keep the challenged cookie session for OTP verification, and rate-limit explicit code resends. Prefer an existing Doodstream API key during health checks and add regression coverage for imports with duplicate accounts, concurrent checks, expired challenges, and session reuse.
2026-08-31 18:45:37 +02:00

144 lines
5.6 KiB
JavaScript

const { createHash } = require('node:crypto');
// Decides which credential an upload task should use for a given hoster.
// Extracted from main.js buildTaskFromAccount so the routing can be unit-tested
// without Electron.
//
// DOODSTREAM SPECIAL CASE: prefer the official doodapi.co API key whenever the
// account has one. The web-login path (username/password) drives doodstream's
// browser upload flow, which hands the filecode back inside an XFileSharing
// HTML form. On long/large uploads that form comes back empty (no fn) because a
// per-page-load sess_id token ages out over the multi-minute upload and/or the
// server-side file-registration callback times out — the upload then "succeeds"
// (bytes sent, HTTP 200) but yields no link. The JSON API returns the filecode
// directly in result[0].filecode and authenticates with a persistent api_key,
// so it has no empty-form failure mode for result retrieval. The API path was
// doodstream's ORIGINAL upload path (present since the initial commit); web
// login was added later only as an alternative for keyless accounts — so
// preferring the key here restores the intended primary path, it doesn't fight
// a deliberate choice. Keyless accounts keep using web login unchanged.
function selectUploadAuth(hoster, account) {
if (!account || typeof account !== 'object') return {};
if (hoster === 'doodstream.com' && account.apiKey) {
return { apiKey: account.apiKey };
}
if (account.authType === 'api' && account.apiKey) {
return { apiKey: account.apiKey };
}
if (account.username && account.password) {
return { username: account.username, password: account.password };
}
if (account.apiKey) {
return { apiKey: account.apiKey };
}
return {};
}
function createDoodstreamOtpCoordinator(options = {}) {
if (typeof options.createUploader !== 'function') throw new TypeError('createUploader is required');
const now = typeof options.now === 'function' ? options.now : Date.now;
const challengeTtlMs = Number.isFinite(Number(options.challengeTtlMs)) ? Math.max(1000, Number(options.challengeTtlMs)) : 10 * 60 * 1000;
const resendCooldownMs = Number.isFinite(Number(options.resendCooldownMs)) ? Math.max(1000, Number(options.resendCooldownMs)) : 60 * 1000;
const maxEntries = Number.isFinite(Number(options.maxEntries)) ? Math.max(1, Math.floor(Number(options.maxEntries))) : 1000;
const states = new Map();
function credentialKey(username, password) {
return createHash('sha256')
.update(String(username || ''))
.update('\0')
.update(String(password || ''))
.digest('hex');
}
function storeState(key, state) {
states.delete(key);
states.set(key, state);
while (states.size > maxEntries) states.delete(states.keys().next().value);
}
function activeState(key) {
const state = states.get(key);
if (!state || state.inFlight) return state || null;
if (state.expiresAt > now()) return state;
states.delete(key);
return null;
}
async function check(input = {}) {
const username = String(input.username || '');
const password = String(input.password || '');
const otp = String(input.otp || '').trim();
const key = credentialKey(username, password);
const existing = activeState(key);
if (existing?.inFlight) return existing.inFlight;
if (otp && !existing?.pending) {
return {
status: 'otp_required',
message: 'OTP-Anfrage ist abgelaufen. Bitte einen neuen Code anfordern.'
};
}
if (!otp && existing?.pending && input.requestNewChallenge !== true) return existing.result;
if (!otp && existing?.pending && now() - existing.requestedAt < resendCooldownMs) return existing.result;
const uploader = otp ? existing.uploader : options.createUploader();
const requestedAt = otp ? existing.requestedAt : now();
const operationId = Symbol('doodstream-otp-check');
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' };
} catch (error) {
if (error?.otpRequired === true) {
const result = { status: 'otp_required', message: error.message || 'OTP erforderlich' };
if (states.get(key)?.operationId === operationId) {
storeState(key, {
operationId,
uploader,
pending: true,
requestedAt: otp ? existing.requestedAt : requestedAt,
expiresAt: now() + challengeTtlMs,
result,
inFlight: null
});
}
return result;
}
if (otp && existing?.pending) {
const result = {
status: 'otp_required',
message: error?.message || 'OTP konnte nicht bestätigt werden'
};
if (states.get(key)?.operationId === operationId) {
storeState(key, {
...existing,
operationId,
uploader,
result,
inFlight: null
});
}
return result;
}
if (states.get(key)?.operationId === operationId) states.delete(key);
throw error;
}
})();
storeState(key, {
operationId,
uploader,
pending: existing?.pending === true,
requestedAt,
expiresAt: existing?.expiresAt || (requestedAt + challengeTtlMs),
result: existing?.result || null,
inFlight: operation
});
return operation;
}
return { check };
}
module.exports = { createDoodstreamOtpCoordinator, selectUploadAuth };