Compare commits

..

2 commits

Author SHA1 Message Date
nocci
b14530537a refactor(config): add network configuration and dependencies for inter-service communication
Some checks failed
build-and-push / lint (push) Failing after 8s
build-and-push / sonar (push) Has been skipped
build-and-push / docker (push) Has been skipped
- Configure flaresolverr and free-games-claimer to use a shared bridge network
- Explicitly set container name for flaresolverr and declare network dependency
- Define custom bridge network `fgc-network` for isolating service communication

This ensures services can reliably communicate over Docker's internal DNS while maintaining network separation from other containers.
2026-03-08 13:22:01 +00:00
nocci
e0c97f8d7c feat[cloudflare]: integrate FlareSolverr for automated Cloudflare challenge resolution
- Add Cloudflare bypass functionality using FlareSolverr service
- Configure FlareSolverr Docker service with environment options
- Add flaresolverr_url config option with default localhost fallback
- Replace manual Cloudflare challenge notification with automated solving attempt
- Create new cloudflare.js module with health check, challenge detection, and solution application
2026-03-08 13:06:46 +00:00
4 changed files with 225 additions and 1 deletions

View file

@ -1,5 +1,18 @@
# start with `docker compose up` # start with `docker compose up`
services: 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: free-games-claimer:
container_name: fgc # is printed in front of every output line 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 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 # - EMAIL=foo@bar.org
# - NOTIFY='tgram://...' # - NOTIFY='tgram://...'
- EG_MODE=new - EG_MODE=new
- FLARESOLVERR_URL=http://flaresolverr:8191/v1
networks:
- fgc-network
depends_on:
- flaresolverr
networks:
fgc-network:
driver: bridge
volumes: volumes:
fgc: fgc:

View file

@ -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 { 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 { setPuppeteerCookies } from './src/cookie.js';
import { getAccountAuth, setAccountAuth } from './src/device-auths.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) // Fetch Free Games from API using page.evaluate (browser context)
const fetchFreeGamesAPI = async page => { const fetchFreeGamesAPI = async page => {
@ -169,6 +170,63 @@ const ensureLoggedIn = async (page, context) => {
return await cfFrame.count() > 0 || await cfText.count() > 0; 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; let loginAttempts = 0;
const MAX_LOGIN_ATTEMPTS = 3; const MAX_LOGIN_ATTEMPTS = 3;
@ -181,7 +239,12 @@ const ensureLoggedIn = async (page, context) => {
await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' }); await page.goto(URL_CLAIM, { waitUntil: 'domcontentloaded' });
if (await isChallenge()) { 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 notify('epic-games (new): Cloudflare challenge, please solve manually in browser.');
await page.waitForTimeout(cfg.login_timeout); await page.waitForTimeout(cfg.login_timeout);
continue; continue;

137
src/cloudflare.js Normal file
View file

@ -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<Object|null>} - 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<boolean>} - 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<boolean>} - 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;
};

View file

@ -35,6 +35,8 @@ export const cfg = {
eg_password: process.env.EG_PASSWORD || process.env.PASSWORD, eg_password: process.env.EG_PASSWORD || process.env.PASSWORD,
eg_otpkey: process.env.EG_OTPKEY, eg_otpkey: process.env.EG_OTPKEY,
eg_parentalpin: process.env.EG_PARENTALPIN, eg_parentalpin: process.env.EG_PARENTALPIN,
// Cloudflare bypass
flaresolverr_url: process.env.FLARESOLVERR_URL || 'http://localhost:8191/v1',
// auth prime-gaming // auth prime-gaming
pg_email: process.env.PG_EMAIL || process.env.EMAIL, pg_email: process.env.PG_EMAIL || process.env.EMAIL,
pg_password: process.env.PG_PASSWORD || process.env.PASSWORD, pg_password: process.env.PG_PASSWORD || process.env.PASSWORD,