import axios from 'axios'; import { firefox } from 'playwright-firefox'; import { authenticator } from 'otplib'; import path from 'node:path'; import { existsSync, readFileSync, writeFileSync } from 'node:fs'; import { resolve, jsonDb, datetime, stealth, filenamify, prompt, notify, html_game_list, handleSIGINT } from './src/util.js'; import { cfg } from './src/config.js'; const URL_CLAIM = 'https://store.epicgames.com/en-US/free-games'; const COOKIES_PATH = path.resolve(cfg.dir.browser, 'epic-cookies.json'); const BEARER_TOKEN_NAME = 'EPIC_BEARER_TOKEN'; const ensureLoggedIn = async (page, context) => { const isLoggedIn = async () => { try { return await page.locator('egs-navigation').getAttribute('isloggedin') === 'true'; } catch (err) { console.error('Error checking login status:', err); return false; } }; const attemptAutoLogin = async () => { // Epic login form if (!cfg.eg_email || !cfg.eg_password) return false; try { await page.goto('https://www.epicgames.com/id/login?lang=en-US&noHostRedirect=true&redirectUrl=' + URL_CLAIM, { waitUntil: 'domcontentloaded', timeout: cfg.login_timeout }); // Add more robust selector handling const emailField = page.locator('input[name="email"], input#email, input[aria-label="Sign in with email"]').first(); const passwordField = page.locator('input[name="password"], input#password').first(); const continueBtn = page.locator('button:has-text("Continue"), button#continue, button[type="submit"]').first(); // Debugging logging console.log('Login page loaded, checking email field'); // step 1: email + continue if (await emailField.count() > 0) { await emailField.fill(cfg.eg_email); await continueBtn.click().catch(err => { console.error('Error clicking continue button:', err); }); } // step 2: password + submit await passwordField.waitFor({ timeout: cfg.login_visible_timeout }); await passwordField.fill(cfg.eg_password); const rememberMe = page.locator('input[name="rememberMe"], #rememberMe').first(); if (await rememberMe.count() > 0) await rememberMe.check().catch(() => { }); await continueBtn.click().catch(async (err) => { console.error('Error clicking continue button:', err); await page.click('button[type="submit"]').catch(() => {}); }); // MFA step try { await page.waitForURL('**/id/login/mfa**', { timeout: cfg.login_timeout }); const otp = cfg.eg_otpkey ? authenticator.generate(cfg.eg_otpkey) : await prompt({ type: 'text', message: 'Enter two-factor sign in code', validate: n => n.toString().length == 6 || 'The code must be 6 digits!' }); const codeInputs = page.locator('input[name^="code-input"]'); if (await codeInputs.count() > 0) { const digits = otp.toString().split(''); for (let i = 0; i < digits.length; i++) { const input = codeInputs.nth(i); await input.fill(digits[i]); } } else { await page.locator('input[name="code-input-0"]').pressSequentially(otp.toString()); } await continueBtn.click().catch(async () => { await page.click('button[type="submit"]').catch(() => {}); }); } catch (mfaError) { console.warn('MFA step failed or not needed:', mfaError); } await page.waitForURL('**/free-games', { timeout: cfg.login_timeout }).catch(err => { console.error('Failed to navigate to free games page:', err); }); return await isLoggedIn(); } catch (err) { console.error('Auto login failed:', err); return false; } }; const isChallenge = async () => { const cfFrame = page.locator('iframe[title*="Cloudflare"], iframe[src*="challenges"]'); const cfText = page.locator('text=Verify you are human'); return await cfFrame.count() > 0 || await cfText.count() > 0; }; let loginAttempts = 0; const MAX_LOGIN_ATTEMPTS = 3; while (!await isLoggedIn() && loginAttempts < MAX_LOGIN_ATTEMPTS) { loginAttempts++; console.error(`Not signed in (Attempt ${loginAttempts}). Trying automatic login, otherwise please login in the browser.`); if (cfg.novnc_port) console.info(`Open http://localhost:${cfg.novnc_port} to login inside the docker container.`); if (!cfg.debug) context.setDefaultTimeout(cfg.login_timeout); console.info(`Login timeout is ${cfg.login_timeout / 1000} seconds!`); await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); if (await isChallenge()) { console.warn('Cloudflare challenge detected. Solve the captcha in the browser (no automation).'); await notify('epic-games (new): Cloudflare challenge, please solve manually in browser.'); await page.waitForTimeout(cfg.login_timeout); continue; } const logged = await attemptAutoLogin(); if (logged) break; console.log('Waiting for manual login in the browser (cookies might be invalid).'); await notify('epic-games (new): please login in browser; cookies invalid or expired.'); if (cfg.headless) { console.log('Run `SHOW=1 node epic-games` to login in the opened browser.'); await context.close(); process.exit(1); } await page.waitForTimeout(cfg.login_timeout); } if (loginAttempts >= MAX_LOGIN_ATTEMPTS) { console.error('Maximum login attempts reached. Exiting.'); await context.close(); process.exit(1); } const user = await page.locator('egs-navigation').getAttribute('displayname'); console.log(`Signed in as ${user}`); if (!cfg.debug) context.setDefaultTimeout(cfg.timeout); return user; };