diff --git a/docker-compose.yml b/docker-compose.yml index f8d797b..f8e1c85 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,18 @@ # start with `docker compose up` services: + flaresolverr: + container_name: flaresolverr + image: flaresolverr/flaresolverr:latest + ports: + - "8191:8191" + environment: + - LOG_LEVEL=info + - LOG_HTML=false + - CAPTCHA_SOLVER=none + restart: unless-stopped + networks: + - fgc-network + free-games-claimer: container_name: fgc # is printed in front of every output line image: ghcr.io/vogler/free-games-claimer # otherwise image name will be free-games-claimer-free-games-claimer @@ -15,6 +28,15 @@ services: # - EMAIL=foo@bar.org # - NOTIFY='tgram://...' - EG_MODE=new + - FLARESOLVERR_URL=http://flaresolverr:8191/v1 + networks: + - fgc-network + depends_on: + - flaresolverr + +networks: + fgc-network: + driver: bridge volumes: fgc: diff --git a/epic-claimer-new.js b/epic-claimer-new.js index 04b8d33..1dfd612 100644 --- a/epic-claimer-new.js +++ b/epic-claimer-new.js @@ -16,6 +16,7 @@ import { cfg } from './src/config.js'; import { EPIC_CLIENT_ID, GRAPHQL_ENDPOINT, FREE_GAMES_PROMOTIONS_ENDPOINT, STORE_HOMEPAGE_EN, EPIC_PURCHASE_ENDPOINT, ID_LOGIN_ENDPOINT } from './src/constants.js'; import { setPuppeteerCookies } from './src/cookie.js'; import { getAccountAuth, setAccountAuth } from './src/device-auths.js'; +import { solveCloudflare, isCloudflareChallenge, waitForCloudflareSolved } from './src/cloudflare.js'; // Fetch Free Games from API using page.evaluate (browser context) const fetchFreeGamesAPI = async page => { @@ -169,6 +170,63 @@ const ensureLoggedIn = async (page, context) => { return await cfFrame.count() > 0 || await cfText.count() > 0; }; + const solveCloudflareChallenge = async () => { + try { + console.log('🔍 Detecting Cloudflare challenge...'); + + // Check if FlareSolverr is available + const flaresolverrUrl = cfg.flaresolverr_url || 'http://localhost:8191/v1'; + const healthResponse = await fetch(`${flaresolverrUrl}/health`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + }); + + if (!healthResponse.ok) { + console.warn('⚠️ FlareSolverr not available at', flaresolverrUrl); + return false; + } + + // Send request to FlareSolverr + const response = await fetch(`${flaresolverrUrl}/request`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + cmd: 'request.get', + url: URL_CLAIM, + maxTimeout: 60000, + session: 'epic-games', + }), + }); + + const data = await response.json(); + + if (data.status !== 'ok') { + console.warn('FlareSolverr failed:', data.message); + return false; + } + + const solution = data.solution; + + // Apply cookies to the browser context + const cookies = solution.cookies.map(cookie => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path || '/', + secure: cookie.secure, + httpOnly: cookie.httpOnly, + })); + + await context.addCookies(cookies); + + console.log('✅ Cloudflare challenge solved by FlareSolverr'); + return true; + } catch (error) { + console.error('FlareSolverr error:', error.message); + return false; + } + }; + let loginAttempts = 0; const MAX_LOGIN_ATTEMPTS = 3; @@ -181,7 +239,12 @@ const ensureLoggedIn = async (page, context) => { await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); if (await isChallenge()) { - console.warn('Cloudflare challenge detected. Solve the captcha in the browser (no automation).'); + console.warn('Cloudflare challenge detected. Attempting to solve with FlareSolverr...'); + const solved = await solveCloudflareChallenge(); + if (solved) { + await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); + continue; + } await notify('epic-games (new): Cloudflare challenge, please solve manually in browser.'); await page.waitForTimeout(cfg.login_timeout); continue; diff --git a/src/cloudflare.js b/src/cloudflare.js new file mode 100644 index 0000000..6c3983c --- /dev/null +++ b/src/cloudflare.js @@ -0,0 +1,137 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { cfg } from './config.js'; + +const FLARESOLVERR_URL = process.env.FLARESOLVERR_URL || 'http://localhost:8191/v1'; + +/** + * Check if FlareSolverr is available + */ +export const checkFlareSolverr = async () => { + try { + const response = await fetch(`${FLARESOLVERR_URL}/health`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.ok; + } catch { + return false; + } +}; + +/** + * Solve Cloudflare challenge using FlareSolverr + * @param {Object} page - Playwright page object + * @param {string} url - The URL to visit + * @returns {Promise} - Solution object with cookies and user agent + */ +export const solveCloudflare = async (page, url) => { + try { + console.log('🔍 Detecting Cloudflare challenge...'); + + // Check if FlareSolverr is available + if (!await checkFlareSolverr()) { + console.warn('⚠️ FlareSolverr not available at', FLARESOLVERR_URL); + return null; + } + + // Send request to FlareSolverr + const response = await fetch(`${FLARESOLVERR_URL}/request`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + cmd: 'request.get', + url: url, + maxTimeout: 60000, + session: 'epic-games', + }), + }); + + const data = await response.json(); + + if (data.status !== 'ok') { + console.warn('FlareSolverr failed:', data.message); + return null; + } + + const solution = data.solution; + + // Apply cookies to the browser context + const cookies = solution.cookies.map(cookie => ({ + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path || '/', + secure: cookie.secure, + httpOnly: cookie.httpOnly, + })); + + // Get the browser context from the page + const context = page.context(); + await context.addCookies(cookies); + + console.log('✅ Cloudflare challenge solved by FlareSolverr'); + + return { + cookies, + userAgent: solution.userAgent, + html: solution.html, + }; + } catch (error) { + console.error('FlareSolverr error:', error.message); + return null; + } +}; + +/** + * Check if Cloudflare challenge is present on the page + * @param {Object} page - Playwright page object + * @returns {Promise} - True if Cloudflare challenge is detected + */ +export const isCloudflareChallenge = async page => { + try { + // Check for Cloudflare iframe + const cfFrame = page.locator('iframe[title*="Cloudflare"], iframe[src*="challenges"]'); + if (await cfFrame.count() > 0) { + return true; + } + + // Check for Cloudflare text + const cfText = page.locator('text=Verify you are human, text=Checking your browser'); + if (await cfText.count() > 0) { + return true; + } + + // Check for specific Cloudflare URLs + const url = page.url(); + if (url.includes('cloudflare') || url.includes('challenges')) { + return true; + } + + return false; + } catch { + return false; + } +}; + +/** + * Wait for Cloudflare challenge to be solved + * @param {Object} page - Playwright page object + * @param {number} timeout - Timeout in milliseconds + * @returns {Promise} - True if challenge is solved + */ +export const waitForCloudflareSolved = async (page, timeout = 60000) => { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + if (!await isCloudflareChallenge(page)) { + return true; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + + return false; +}; diff --git a/src/config.js b/src/config.js index 7702984..89b6e04 100644 --- a/src/config.js +++ b/src/config.js @@ -35,6 +35,8 @@ export const cfg = { eg_password: process.env.EG_PASSWORD || process.env.PASSWORD, eg_otpkey: process.env.EG_OTPKEY, eg_parentalpin: process.env.EG_PARENTALPIN, + // Cloudflare bypass + flaresolverr_url: process.env.FLARESOLVERR_URL || 'http://localhost:8191/v1', // auth prime-gaming pg_email: process.env.PG_EMAIL || process.env.EMAIL, pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,