File: /home/g/e/y/geyhairs/www/kvih6/index.php
<?php
/**
* ============================================
* ANTI-BOT GATEWAY (index.php)
* بوابة حماية ضد البوتات + reCAPTCHA v3
* ============================================
*
* كيف يشتغل:
* 1. الزائر يدخل على هذه الصفحة أولاً
* 2. السيرفر يعمل فحوصات سريعة (IP, User-Agent, Headers, Cloudflare)
* 3. لو الفحوصات الأولية فشلت → حظر مباشر
* 4. لو نجحت → يعرض صفحة تحدي JavaScript تفحص السلوك + reCAPTCHA v3
* 5. لو الزائر اجتاز التحدي → يحول للصفحة الحقيقية
*/
// ============================================
// Polyfills لدوال PHP 8 (لتشتغل على PHP 7.x)
// ============================================
if (!function_exists('str_contains')) {
function str_contains(string $haystack, string $needle): bool {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
if (!function_exists('str_starts_with')) {
function str_starts_with(string $haystack, string $needle): bool {
return $needle === '' || strncmp($haystack, $needle, strlen($needle)) === 0;
}
}
if (!function_exists('str_ends_with')) {
function str_ends_with(string $haystack, string $needle): bool {
return $needle === '' || substr($haystack, -strlen($needle)) === $needle;
}
}
// إعدادات الجلسة - لازم تطابق guard.php بالظبط عشان يشاركوا نفس الجلسة
if (session_status() === PHP_SESSION_NONE) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
// 'secure' => true, // فعّلها لما يصير الموقع HTTPS
]);
session_start();
}
// ============================================
// الإعدادات
// ============================================
const CONFIG = [
'redirect_to' => 'info.php', // الصفحة الحقيقية
'challenge_timeout' => 1500, // ميلي ثانية - الحد الأدنى لحل التحدي (البوت أسرع)
'challenge_max_time' => 60000, // الحد الأقصى (60 ثانية)
'rate_limit_per_min' => 30, // طلبات لكل IP بالدقيقة
'session_token_lifetime'=> 3600, // عمر التوكن بالثواني
'log_file' => __DIR__ . '/logs/security.log',
'rate_limit_dir' => __DIR__ . '/cache/ratelimit/',
'secret_key' => 'CHANGE_THIS_TO_RANDOM_STRING_xK9mP2vN7qL4', // غيره!
'require_cloudflare' => false, // true لو الموقع خلف Cloudflare بس
'bot_score_threshold' => 30, // أقل من هذا = بوت (من Cloudflare)
// ---- إعدادات reCAPTCHA v3 ----
'recaptcha_sitekey' => '6LdQAyotAAAAAOGuUkBP4_NdqYKCe9OrxTGTHwUe',
'recaptcha_secret' => '6LdQAyotAAAAAFhn7-Ej-TCDZtSb5bwvW7xVTslS',
'recaptcha_min_score' => 0.5, // 0.0 = بوت مؤكد، 1.0 = إنسان مؤكد
'recaptcha_action' => 'gateway', // اسم الـ action للتحقق منه
'recaptcha_hard_block' => true, // true = احظر مباشرة لو السكور تحت الحد (hard fail)
// false = أضف نقاط فقط (soft - الطريقة القديمة)
'recaptcha_block_score' => 0.5, // [صارم] تحت هذا = حظر فوري. (كان 0.3، رفعناه لـ 0.5)
'automation_hard_block' => true, // true = احظر فوراً لو في علامة أتمتة قاطعة (webdriver/selenium)
// حتى لو reCAPTCHA أعطى سكور عالي. (جوجل ما يقدر يبرّئ بوت واضح)
'datacenter_penalty' => 35, // نقاط تُضاف لو الـ IP من نطاق datacenter (لما strict مُطفأ)
// ============================================
// ⚡ الوضع الصارم (STRICT MODE) - بوابة دفع
// ============================================
// ⚠️ الوضع الصارم بيحظر بقوة أكبر، بس ممكن يحظر نسبة صغيرة من الزبائن
// الحقيقيين (اللي عندهم ad-blocker، أو على VPN، أو خلل شبكة عابر).
// لو لقيت إنك بتخسر زبائن، خلّي أي مفتاح تحت = false للتخفيف.
'strict_mode' => true, // المفتاح الرئيسي للوضع الصارم
// 1) reCAPTCHA إلزامي: أي فشل (حتى browser-error تقني) = حظر فوري.
// معناه: أي زائر ما بيشغّل JS أو reCAPTCHA فشلت عنده = محظور.
'recaptcha_require' => true,
// 3) datacenter/VPN: حظر فوري لكل النطاقات المعروفة (مش نقاط).
'datacenter_hard_block' => true,
];
// إنشاء المجلدات لو مش موجودة
@mkdir(dirname(CONFIG['log_file']), 0755, true);
@mkdir(CONFIG['rate_limit_dir'], 0755, true);
// ============================================
// التشغيل الرئيسي
// ============================================
$gateway = new AntiBotGateway(CONFIG);
// لو الطلب POST من صفحة التحدي → تحقق من التحدي
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['challenge_response'])) {
$gateway->verifyChallenge();
exit;
}
// لو الزائر اجتاز التحدي مسبقاً (session valid) → حوله مباشرة
if ($gateway->hasValidSession()) {
header('Location: ' . CONFIG['redirect_to']);
exit;
}
// شغل الفحوصات الأولية
$result = $gateway->runInitialChecks();
if ($result['action'] === 'block') {
$gateway->blockVisitor($result['reason']);
exit;
}
// اعرض صفحة التحدي
$gateway->showChallengePage();
// ============================================
// CLASS: AntiBotGateway
// ============================================
class AntiBotGateway
{
private array $config;
private string $ip;
private string $userAgent;
private array $headers;
public function __construct(array $config)
{
$this->config = $config;
$this->ip = $this->getRealIP();
$this->userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
$this->headers = $this->getAllHeaders();
}
/**
* فحوصات أولية سريعة على السيرفر
*/
public function runInitialChecks(): array
{
$score = 0;
$reasons = [];
// 0) حظر محركات البحث (Googlebot, Bingbot...)
// ⚠️ هذي صفحة بوابة دفع خاصة - ما بدنا أي بوت، حتى محركات البحث.
// ما بدنا الصفحة تنفهرس ولا تظهر بنتائج البحث أصلاً (خصوصية الدفع).
// نتحقق بالـ IP مش بالـ UA (الـ UA سهل تزييفه).
if ($this->isKnownBotIP($this->ip)) {
return ['action' => 'block', 'reason' => 'search_engine_bot_blocked'];
}
// 1) فحص Cloudflare (لو متاح)
$cfBotScore = $_SERVER['HTTP_CF_BOT_SCORE'] ?? null;
$cfThreatScore = $_SERVER['HTTP_CF_THREAT_SCORE'] ?? null;
$cfVerifiedBot = $_SERVER['HTTP_CF_VERIFIED_BOT'] ?? null;
if ($this->config['require_cloudflare'] && !isset($_SERVER['HTTP_CF_RAY'])) {
return ['action' => 'block', 'reason' => 'not_through_cloudflare'];
}
if ($cfBotScore !== null && (int)$cfBotScore < $this->config['bot_score_threshold']) {
$score += 50;
$reasons[] = "cf_bot_score:$cfBotScore";
}
if ($cfThreatScore !== null && (int)$cfThreatScore > 30) {
$score += 30;
$reasons[] = "cf_threat_score:$cfThreatScore";
}
// 2) فحص User-Agent
if (empty($this->userAgent) || strlen($this->userAgent) < 20) {
$score += 50;
$reasons[] = 'empty_or_short_ua';
} elseif ($this->isBadBot($this->userAgent)) {
$score += 60;
$reasons[] = 'bad_bot_ua';
} elseif ($this->isAncientBrowser($this->userAgent)) {
// متصفحات أثرية (MSIE 8/9, Trident قديم) - ما حدا بيستخدمها اليوم
// غالباً بوت/سكرابر بيتنكّر. إشارة قوية.
$score += 40;
$reasons[] = 'ancient_browser';
}
// 3) فحص الـ Headers الأساسية (المتصفحات الحقيقية ترسلها)
$requiredHeaders = ['Accept', 'Accept-Language', 'Accept-Encoding'];
foreach ($requiredHeaders as $h) {
if (empty($this->headers[$h])) {
$score += 15;
$reasons[] = "missing_header:$h";
}
}
// 4) فحص Accept-Language (البوتات غالباً ما ترسله أو يكون "*")
$acceptLang = $this->headers['Accept-Language'] ?? '';
if ($acceptLang === '*' || $acceptLang === '') {
$score += 10;
$reasons[] = 'invalid_accept_lang';
}
// 5) فحص rate limit
if (!$this->checkRateLimit()) {
return ['action' => 'block', 'reason' => 'rate_limit_exceeded'];
}
// 6) فحص الـ IP في الـ blacklist
if ($this->isBlacklisted()) {
return ['action' => 'block', 'reason' => 'ip_blacklisted'];
}
// 7) فحص Headers مشبوهة (أدوات الـ scraping)
$suspiciousHeaders = ['HTTP_X_FORWARDED_FOR_PROXY', 'HTTP_VIA', 'HTTP_X_PROXY_ID'];
foreach ($suspiciousHeaders as $h) {
if (isset($_SERVER[$h])) {
$score += 5;
$reasons[] = "proxy_header:$h";
}
}
// 8) فحص الـ Headers من الـ HTTP Client Hints (المتصفحات الحديثة)
// غيابها مش دليل قاطع بس مع عوامل ثانية يقوي الشك
if (!isset($this->headers['Sec-Ch-Ua']) && !isset($this->headers['Sec-Fetch-Site'])) {
$score += 5;
$reasons[] = 'no_modern_headers';
}
// 9) فحص نطاقات الـ datacenter/hosting
// المستخدم المنزلي الحقيقي ما بيجي من datacenter. ظهور IP من
// نطاق hosting (OVH/AWS/إلخ) إشارة قوية على بوت/سكرابر.
if ($this->isDatacenterIP($this->ip)) {
// [صارم] حظر فوري لكل نطاقات datacenter/VPN المعروفة
if (($this->config['strict_mode'] ?? false)
&& ($this->config['datacenter_hard_block'] ?? false)) {
return ['action' => 'block', 'reason' => 'datacenter_ip_strict'];
}
// [عادي] نقاط بس (زبون VPN يقدر يمرق)
$score += $this->config['datacenter_penalty'];
$reasons[] = 'datacenter_ip';
}
// قرار نهائي
$this->log('initial_check', [
'score' => $score,
'reasons' => $reasons,
'ip' => $this->ip,
'ua' => substr($this->userAgent, 0, 100),
]);
if ($score >= 80) {
return ['action' => 'block', 'reason' => 'high_risk_score:' . $score];
}
return ['action' => 'challenge', 'score' => $score];
}
/**
* عرض صفحة التحدي (JavaScript challenge + reCAPTCHA v3)
*/
public function showChallengePage(): void
{
// إنشاء توكن للتحدي
$challengeToken = bin2hex(random_bytes(16));
$challengeTime = microtime(true);
$expectedAnswer = $this->generateMathChallenge();
$_SESSION['challenge'] = [
'token' => $challengeToken,
'time' => $challengeTime,
'answer' => $expectedAnswer['answer'],
'ip' => $this->ip,
'ua_hash' => md5($this->userAgent),
];
$nonce = base64_encode(random_bytes(16));
// ملاحظة: CSP عُدّلت للسماح بسكربتات reCAPTCHA من جوجل
header("Content-Security-Policy: script-src 'self' 'nonce-$nonce' https://www.google.com https://www.gstatic.com; frame-src https://www.google.com");
header('X-Frame-Options: DENY');
header('X-Content-Type-Options: nosniff');
header('X-Robots-Tag: noindex, nofollow, noarchive');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex, nofollow">
<title></title>
<style>
html, body { margin: 0; padding: 0; background: #ffffff; height: 100%; }
/* إخفاء شارة reCAPTCHA (اختياري - راجع الملاحظة بالأسفل) */
.grecaptcha-badge { visibility: hidden; }
</style>
<script nonce="<?= $nonce ?>" src="https://www.google.com/recaptcha/api.js?render=<?= $this->config['recaptcha_sitekey'] ?>"></script>
</head>
<body>
<script nonce="<?= $nonce ?>" id="challenge-data" type="application/json">
{
"token": "<?= $challengeToken ?>",
"a": <?= $expectedAnswer['a'] ?>,
"b": <?= $expectedAnswer['b'] ?>,
"op": "<?= $expectedAnswer['op'] ?>",
"minTime": <?= $this->config['challenge_timeout'] ?>,
"sitekey": "<?= $this->config['recaptcha_sitekey'] ?>",
"action": "<?= $this->config['recaptcha_action'] ?>"
}
</script>
<script nonce="<?= $nonce ?>">
(function() {
'use strict';
const data = JSON.parse(document.getElementById('challenge-data').textContent);
const startTime = performance.now();
// إشارات السلوك البشري (لتحليلها لاحقاً)
const behaviorSignals = {
mouseMovements: 0,
keyPresses: 0,
scrollEvents: 0,
touchEvents: 0,
firstInteraction: null,
screenWidth: screen.width,
screenHeight: screen.height,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
colorDepth: screen.colorDepth,
pixelRatio: window.devicePixelRatio,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
language: navigator.language,
languages: navigator.languages ? navigator.languages.join(',') : '',
platform: navigator.platform,
hardwareConcurrency: navigator.hardwareConcurrency || 0,
deviceMemory: navigator.deviceMemory || 0,
cookiesEnabled: navigator.cookieEnabled,
doNotTrack: navigator.doNotTrack,
webdriver: navigator.webdriver || false, // علامة قوية للأتمتة!
plugins: navigator.plugins ? navigator.plugins.length : 0,
hasTouch: 'ontouchstart' in window,
hasWebGL: detectWebGL(),
canvasFingerprint: getCanvasFingerprint(),
};
function detectWebGL() {
try {
const canvas = document.createElement('canvas');
return !!(canvas.getContext('webgl') || canvas.getContext('experimental-webgl'));
} catch(e) { return false; }
}
function getCanvasFingerprint() {
try {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.textBaseline = 'top';
ctx.font = '14px Arial';
ctx.fillStyle = '#f60';
ctx.fillRect(0, 0, 50, 20);
ctx.fillStyle = '#069';
ctx.fillText('AntiBot', 2, 15);
return canvas.toDataURL().substring(0, 50);
} catch(e) { return 'error'; }
}
// رصد التفاعلات
document.addEventListener('mousemove', () => {
behaviorSignals.mouseMovements++;
if (!behaviorSignals.firstInteraction) {
behaviorSignals.firstInteraction = performance.now() - startTime;
}
}, { passive: true });
document.addEventListener('keydown', () => behaviorSignals.keyPresses++, { passive: true });
document.addEventListener('scroll', () => behaviorSignals.scrollEvents++, { passive: true });
document.addEventListener('touchstart', () => behaviorSignals.touchEvents++, { passive: true });
// كشف أدوات الأتمتة الشائعة
const automationFlags = {
webdriver: navigator.webdriver === true,
phantom: !!window._phantom || !!window.callPhantom,
selenium: !!window.document.documentElement.getAttribute('selenium') ||
!!window.document.documentElement.getAttribute('webdriver') ||
!!window.document.documentElement.getAttribute('driver'),
puppeteer: navigator.userAgent.toLowerCase().includes('headless'),
chromeAutomation: !!window.chrome && !window.chrome.runtime && navigator.webdriver,
};
// حل التحدي الرياضي
let answer;
switch(data.op) {
case '+': answer = data.a + data.b; break;
case '-': answer = data.a - data.b; break;
case '*': answer = data.a * data.b; break;
default: answer = 0;
}
// قفل يمنع الإرسال أكثر من مرة (يمنع توكن مكرر = browser-error/duplicate)
let submitted = false;
// إرسال النتيجة بعد جلب توكن reCAPTCHA + انتظار الحد الأدنى من الوقت
function submit() {
// لو أُرسل مسبقاً، لا تكرر أبداً
if (submitted) return;
const elapsed = performance.now() - startTime;
// البشر يحتاجون وقت - انتظر الحد الأدنى
if (elapsed < data.minTime) {
setTimeout(submit, data.minTime - elapsed + 100);
return;
}
// اقفل فوراً قبل أي عملية async (يضمن مرة واحدة بس)
submitted = true;
// اجلب توكن reCAPTCHA v3 طازج لحظة الإرسال مباشرة، ثم أرسل
grecaptcha.ready(function() {
grecaptcha.execute(data.sitekey, { action: data.action })
.then(function(recaptchaToken) {
// لو ما رجع توكن صالح، لا ترسل (يمنع browser-error من توكن فاضي)
if (!recaptchaToken) {
window.stop();
return;
}
const payload = {
challenge_response: answer,
token: data.token,
elapsed: performance.now() - startTime,
behavior: behaviorSignals,
automation: automationFlags,
recaptcha_token: recaptchaToken,
};
fetch(window.location.href, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: 'challenge_response=' + encodeURIComponent(JSON.stringify(payload)),
credentials: 'same-origin',
})
.then(r => r.json())
.then(res => {
if (res.success && res.redirect) {
window.location.replace(res.redirect);
} else {
// فشل صامت - الصفحة تبقى بيضاء
window.stop();
}
})
.catch(() => {
// خطأ صامت
});
})
.catch(function() {
// فشل تحميل/تنفيذ reCAPTCHA - خطأ صامت
window.stop();
});
});
}
// ابدأ العملية مرة واحدة (القفل داخل submit يمنع أي تكرار)
setTimeout(submit, data.minTime);
})();
</script>
</body>
</html>
<?php
}
/**
* التحقق من إجابة التحدي
*/
public function verifyChallenge(): void
{
header('Content-Type: application/json');
if (!isset($_SESSION['challenge'])) {
echo json_encode(['success' => false, 'message' => 'no_challenge_session']);
return;
}
$challenge = $_SESSION['challenge'];
$rawPayload = $_POST['challenge_response'] ?? '';
$payload = json_decode($rawPayload, true);
if (!$payload || !is_array($payload)) {
echo json_encode(['success' => false, 'message' => 'invalid_payload']);
return;
}
$score = 0;
$reasons = [];
// 1) تحقق من التوكن
if (($payload['token'] ?? '') !== $challenge['token']) {
$this->log('challenge_failed', ['reason' => 'invalid_token', 'ip' => $this->ip]);
echo json_encode(['success' => false, 'message' => 'invalid_token']);
return;
}
// 2) تحقق من الـ IP/UA ما تغيرت
if ($challenge['ip'] !== $this->ip || $challenge['ua_hash'] !== md5($this->userAgent)) {
$this->log('challenge_failed', ['reason' => 'session_hijack', 'ip' => $this->ip]);
echo json_encode(['success' => false, 'message' => 'session_mismatch']);
return;
}
// 3) تحقق من الإجابة الرياضية
if ((int)($payload['challenge_response'] ?? -999) !== $challenge['answer']) {
$this->log('challenge_failed', ['reason' => 'wrong_answer', 'ip' => $this->ip]);
echo json_encode(['success' => false, 'message' => 'wrong_answer']);
return;
}
// 4) فحص reCAPTCHA v3
$recaptcha = $this->verifyRecaptcha($payload['recaptcha_token'] ?? '');
if ($recaptcha['success']) {
$rScore = $recaptcha['score'];
// --- hard fail: سكور واطي مؤكد = حظر فوري ---
// ملاحظة: نحظر فقط لو جوجل رجّع سكور واضح إنه بوت (block_score).
// المنطقة الرمادية (بين block_score و min_score) بتاخد نقاط بس،
// عشان باقي الفحوصات السلوكية تحكم فيها.
if ($this->config['recaptcha_hard_block']
&& $rScore < $this->config['recaptcha_block_score']) {
$this->log('challenge_failed', [
'reason' => 'recaptcha_hard_block',
'recaptcha_score' => $rScore,
'ip' => $this->ip,
]);
unset($_SESSION['challenge']);
$this->addToBlacklist($this->ip, 'recaptcha_low_score:' . $rScore);
echo json_encode(['success' => false, 'message' => 'verification_failed']);
return;
}
// منطقة رمادية: تحت min_score بس فوق block_score = نقاط
if ($rScore < $this->config['recaptcha_min_score']) {
$score += 40;
$reasons[] = 'recaptcha_low_score:' . $rScore;
}
// تحقق من تطابق الـ action (يمنع استخدام توكن من صفحة ثانية)
if ($recaptcha['action'] !== $this->config['recaptcha_action']) {
$score += 25;
$reasons[] = 'recaptcha_action_mismatch:' . $recaptcha['action'];
}
} else {
// التوكن فشل.
$err = $recaptcha['error'];
// [صارم] reCAPTCHA إلزامي: أي فشل = حظر فوري (حتى browser-error تقني).
// معناه: أي زائر ما بيشغّل JS، أو reCAPTCHA فشلت عنده لأي سبب = محظور.
// ⚠️ هذا بيحظر نسبة صغيرة من الزبائن الحقيقيين (ad-blocker/شبكة عابرة).
if (($this->config['strict_mode'] ?? false)
&& ($this->config['recaptcha_require'] ?? false)) {
$this->log('challenge_failed', [
'reason' => 'recaptcha_required_strict:' . $err,
'ip' => $this->ip,
]);
unset($_SESSION['challenge']);
$this->addToBlacklist($this->ip, 'recaptcha_required_strict:' . $err);
echo json_encode(['success' => false, 'message' => 'verification_failed']);
return;
}
// [عادي] تمييز الأخطاء العابرة عن الجدية (لما strict مُطفأ):
// browser-error / timeout-or-duplicate / verify_unreachable = عابرة
$transientErrors = ['browser-error', 'timeout-or-duplicate', 'verify_unreachable'];
$isTransient = false;
foreach ($transientErrors as $te) {
if (str_contains($err, $te)) { $isTransient = true; break; }
}
if ($isTransient) {
$score += 15; // عقوبة خفيفة - باقي الفحوصات بتحكم
$reasons[] = 'recaptcha_transient:' . $err;
} else {
// أخطاء جدية (invalid-input-response, missing-input-response, إلخ)
// غالباً توكن مزيّف أو مفقود = إشارة بوت أقوى
$score += 35;
$reasons[] = 'recaptcha_hard_fail:' . $err;
}
}
// 5) تحقق من الوقت (البوت بيحل بسرعة، البشر أبطأ)
$elapsed = (float)($payload['elapsed'] ?? 0);
if ($elapsed < $this->config['challenge_timeout'] * 0.8) {
$score += 30;
$reasons[] = 'too_fast';
}
if ($elapsed > $this->config['challenge_max_time']) {
$score += 20;
$reasons[] = 'too_slow';
}
// 6) فحص علامات الأتمتة
$automation = $payload['automation'] ?? [];
$definiteAutomation = false;
foreach ($automation as $key => $val) {
if ($val === true) {
$score += 40;
$reasons[] = "automation:$key";
// webdriver/selenium/phantom = علامات قاطعة (مش ظنّية)
if (in_array($key, ['webdriver', 'selenium', 'phantom'], true)) {
$definiteAutomation = true;
}
}
}
// 7) فحص السلوك
$behavior = $payload['behavior'] ?? [];
if (($behavior['webdriver'] ?? false) === true) {
$score += 50;
$reasons[] = 'webdriver_flag';
$definiteAutomation = true;
}
// --- حظر قاطع على الأتمتة (جوجل ما يقدر يبرّئ بوت واضح) ---
// لو في علامة أتمتة مؤكدة، احظر فوراً مهما كان سكور reCAPTCHA.
// هاد بيسدّ الثغرة: بوت بـ residential proxy ياخد سكور عالي من جوجل
// بس webdriver بيفضحه. السلوك القاطع بيغلب سكور جوجل.
if ($this->config['automation_hard_block'] && $definiteAutomation) {
$this->log('challenge_failed', [
'reason' => 'automation_detected',
'ip' => $this->ip,
'recaptcha_score' => $recaptcha['score'] ?? null,
'flags' => $reasons,
]);
unset($_SESSION['challenge']);
$this->addToBlacklist($this->ip, 'automation_hard_block');
echo json_encode(['success' => false, 'message' => 'verification_failed']);
return;
}
// ما في أي حركة ماوس ولا لمس ولا scroll
// ملاحظة: عقوبة خفيفة (5) لأن صفحة البوابة بيضاء وبتتحوّل بسرعة،
// فناس حقيقيين كتير ما بيتفاعلوا معها. ما بنعتمد عليها كإشارة قوية.
$totalInteractions = ($behavior['mouseMovements'] ?? 0)
+ ($behavior['touchEvents'] ?? 0)
+ ($behavior['scrollEvents'] ?? 0)
+ ($behavior['keyPresses'] ?? 0);
if ($totalInteractions === 0 && $elapsed > 2000) {
$score += 5;
$reasons[] = 'no_interaction';
}
// أبعاد شاشة غير منطقية (Headless browsers)
$sw = (int)($behavior['screenWidth'] ?? 0);
$sh = (int)($behavior['screenHeight'] ?? 0);
if ($sw === 0 || $sh === 0 || $sw < 100 || $sh < 100) {
$score += 25;
$reasons[] = 'invalid_screen';
}
// ما في WebGL ولا canvas (Headless غالباً)
if (!($behavior['hasWebGL'] ?? true)) {
$score += 15;
$reasons[] = 'no_webgl';
}
if (($behavior['canvasFingerprint'] ?? '') === 'error') {
$score += 10;
$reasons[] = 'canvas_error';
}
// ما في plugins ولا hardware concurrency (Headless)
if (($behavior['hardwareConcurrency'] ?? 0) === 0) {
$score += 10;
$reasons[] = 'no_hw_concurrency';
}
// 8) القرار النهائي
$this->log('challenge_complete', [
'ip' => $this->ip,
'score' => $score,
'reasons' => $reasons,
'elapsed' => $elapsed,
'recaptcha_score' => $recaptcha['score'] ?? null,
]);
// مسح التحدي
unset($_SESSION['challenge']);
if ($score >= 50) {
// مشبوه → احظر
$this->addToBlacklist($this->ip, 'failed_behavior_check');
echo json_encode(['success' => false, 'message' => 'verification_failed']);
return;
}
// نجح → أنشئ جلسة موثقة
$_SESSION['verified'] = [
'token' => bin2hex(random_bytes(32)),
'verified_at' => time(),
'expires' => time() + $this->config['session_token_lifetime'],
'ip' => $this->ip,
'ua_hash' => md5($this->userAgent),
];
echo json_encode([
'success' => true,
'redirect' => $this->config['redirect_to'],
]);
}
/**
* فحص هل في جلسة موثقة سابقة
*/
public function hasValidSession(): bool
{
if (!isset($_SESSION['verified'])) return false;
$v = $_SESSION['verified'];
if ($v['expires'] < time()) {
unset($_SESSION['verified']);
return false;
}
if ($v['ip'] !== $this->ip || $v['ua_hash'] !== md5($this->userAgent)) {
unset($_SESSION['verified']);
return false;
}
return true;
}
/**
* صفحة الحظر
*/
public function blockVisitor(string $reason): void
{
$this->log('blocked', ['ip' => $this->ip, 'reason' => $reason, 'ua' => substr($this->userAgent, 0, 200)]);
http_response_code(403);
header('Content-Type: text/html; charset=UTF-8');
header('X-Robots-Tag: noindex, nofollow, noarchive');
?>
<!DOCTYPE html>
<html>
<head><meta charset="UTF-8"><title></title>
<style>html,body{margin:0;padding:0;background:#fff;height:100%;}</style>
</head><body></body></html>
<?php
}
// ============================================
// الدوال المساعدة
// ============================================
private function getRealIP(): string
{
// ترتيب الأولوية: Cloudflare > X-Real-IP > X-Forwarded-For > REMOTE_ADDR
foreach (['HTTP_CF_CONNECTING_IP', 'HTTP_X_REAL_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR'] as $key) {
if (!empty($_SERVER[$key])) {
$ip = explode(',', $_SERVER[$key])[0];
$ip = trim($ip);
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
}
return '0.0.0.0';
}
private function getAllHeaders(): array
{
if (function_exists('getallheaders')) return getallheaders() ?: [];
$headers = [];
foreach ($_SERVER as $k => $v) {
if (str_starts_with($k, 'HTTP_')) {
$name = str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($k, 5)))));
$headers[$name] = $v;
}
}
return $headers;
}
private function isBadBot(string $ua): bool
{
$bad = [
'curl', 'wget', 'python-requests', 'python-urllib', 'go-http-client',
'java/', 'okhttp', 'axios', 'node-fetch', 'phantomjs',
'headlesschrome', 'selenium', 'puppeteer', 'playwright',
'scrapy', 'masscan', 'nmap', 'sqlmap', 'nikto', 'libwww-perl',
];
$uaLower = strtolower($ua);
// استثناء البوتات الجيدة (محركات البحث)
$good = ['googlebot', 'bingbot', 'duckduckbot', 'slurp', 'yandexbot', 'baiduspider'];
foreach ($good as $g) {
if (str_contains($uaLower, $g)) return false;
}
foreach ($bad as $b) {
if (str_contains($uaLower, $b)) return true;
}
return false;
}
/**
* كشف المتصفحات الأثرية (MSIE 8/9, Trident قديم)
* هاي المتصفحات تقريباً منقرضة، فظهورها اليوم غالباً بوت متنكّر.
* ملاحظة: MSIE 11 / Trident 7 لسه موجود بنسبة ضئيلة، فما بنعاقبه هون.
*/
private function isAncientBrowser(string $ua): bool
{
// MSIE 6/7/8/9/10 = قديمة جداً
if (preg_match('/MSIE [6-9]\.0/i', $ua)) return true;
if (preg_match('/MSIE 10\.0/i', $ua)) return true;
// Trident/4.0 (IE8), /5.0 (IE9), /6.0 (IE10) = قديمة
if (preg_match('/Trident\/[4-6]\.0/i', $ua)) return true;
return false;
}
/**
* كشف هل الـ IP من نطاق datacenter/hosting معروف.
*
* ⚠️ ملاحظات مهمة:
* - هذي قائمة جزئية لأشهر الموفّرين. مش شاملة - رح يفوت بعض البوتات.
* - الحل الأقوى والأشمل هو Cloudflare (HTTP_CF_BOT_SCORE) لما يصير الدومين Active.
* - النطاقات بتتغيّر، فحدّثها كل فترة من قوائم ASN العامة.
* - النطاقات أدناه تشمل OVH (اللي ظهر بكثرة في اللوج)، AWS، Google Cloud،
* DigitalOcean، Hetzner، Azure، Linode، وغيرهم.
*/
private function isDatacenterIP(string $ip): bool
{
// فقط IPv4 (الـ IPv6 نتركه لـ Cloudflare لاحقاً)
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
// قائمة CIDR لنطاقات hosting شائعة
$ranges = [
// ---- OVH (ظهر بكثرة في لوجك) ----
'51.38.0.0/16', '51.68.0.0/16', '51.75.0.0/16', '51.77.0.0/16',
'51.79.0.0/16', '51.83.0.0/16', '51.89.0.0/16', '51.91.0.0/16',
'51.195.0.0/16', '51.222.0.0/16', '51.254.0.0/16', '51.255.0.0/16',
'54.36.0.0/16', '54.37.0.0/16', '54.38.0.0/16', '54.39.0.0/16',
'79.137.0.0/16', '91.121.0.0/16', '91.134.0.0/16', '92.222.0.0/16',
'94.23.0.0/16', '137.74.0.0/16', '139.99.0.0/16', '141.94.0.0/16',
'141.95.0.0/16', '145.239.0.0/16', '146.59.0.0/16', '147.135.0.0/16',
'149.202.0.0/16', '151.80.0.0/16', '158.69.0.0/16', '164.132.0.0/16',
'167.114.0.0/16', '176.31.0.0/16', '178.32.0.0/16', '188.165.0.0/16',
'192.99.0.0/16', '193.70.0.0/16', '198.27.64.0/18', '213.32.0.0/16',
'217.182.0.0/16',
// ---- OVH نطاقات إضافية (ظهرت في اللوج لاحقاً) ----
// ⚠️ ملاحظة: شِلنا 185.106.112.0/22 و 194.223.0.0/16 لأنهم فلّغوا
// زوار حقيقيين عدّوا بـ reCAPTCHA 0.9. النطاقات الكبيرة أحياناً
// فيها مستخدمين شرعيين. الدرس: تأكّد من اللوج قبل إضافة أي نطاق.
'5.135.0.0/16', '5.196.0.0/16', '37.59.0.0/16', '37.187.0.0/16',
'46.105.0.0/16', '87.98.0.0/16', '142.44.0.0/16', '144.217.0.0/16',
'146.120.0.0/16', '178.33.0.0/16', '198.50.128.0/17',
// ---- AWS ----
'3.0.0.0/9', '13.32.0.0/15', '15.177.0.0/16', '18.32.0.0/11',
'34.192.0.0/10', '35.152.0.0/13', '44.192.0.0/10', '52.0.0.0/11',
'54.144.0.0/12', '54.224.0.0/11', '99.77.128.0/18',
// ---- Google Cloud ----
'34.64.0.0/10', '35.184.0.0/13', '35.192.0.0/14', '35.196.0.0/15',
'35.198.0.0/16', '35.200.0.0/13', '104.196.0.0/14', '130.211.0.0/16',
'146.148.0.0/17',
// ---- DigitalOcean ----
'104.131.0.0/16', '104.236.0.0/16', '128.199.0.0/16', '134.209.0.0/16',
'138.197.0.0/16', '138.68.0.0/16', '142.93.0.0/16', '143.110.0.0/16',
'146.190.0.0/16', '157.230.0.0/16', '159.65.0.0/16', '159.89.0.0/16',
'161.35.0.0/16', '164.90.0.0/16', '165.227.0.0/16', '167.71.0.0/16',
'167.99.0.0/16', '178.62.0.0/16', '188.166.0.0/16', '206.189.0.0/16',
// ---- Hetzner ----
'5.9.0.0/16', '78.46.0.0/15', '88.198.0.0/16', '94.130.0.0/16',
'95.216.0.0/15', '116.202.0.0/15', '128.140.0.0/16', '135.181.0.0/16',
'136.243.0.0/16', '138.201.0.0/16', '142.132.0.0/16', '144.76.0.0/16',
'148.251.0.0/16', '157.90.0.0/16', '159.69.0.0/16', '162.55.0.0/16',
'167.235.0.0/16', '168.119.0.0/16', '176.9.0.0/16', '188.34.0.0/16',
'195.201.0.0/16', '213.239.192.0/18',
// ---- Microsoft Azure (عيّنة) ----
'13.64.0.0/11', '20.33.0.0/16', '20.34.0.0/15', '20.36.0.0/14',
'20.40.0.0/13', '20.128.0.0/16', '40.64.0.0/10', '52.224.0.0/11',
'104.40.0.0/13',
// ---- Linode/Akamai ----
'45.33.0.0/16', '45.56.0.0/16', '45.79.0.0/16', '50.116.0.0/16',
'66.175.208.0/20', '69.164.192.0/19', '72.14.176.0/20', '96.126.96.0/19',
'139.144.0.0/16', '170.187.0.0/16', '172.104.0.0/15', '173.255.192.0/18',
'192.46.208.0/20', '198.58.96.0/19',
// ---- Contabo / Vultr / آخرين ----
'38.242.0.0/16', '62.171.128.0/17', '144.91.0.0/16', '161.97.0.0/16',
'167.86.0.0/16', '173.249.0.0/16', '207.180.192.0/18', // Contabo
'45.32.0.0/16', '45.63.0.0/16', '45.76.0.0/16', '66.42.0.0/17',
'108.61.0.0/16', '149.28.0.0/16', '207.246.64.0/18', // Vultr
];
$ipLong = ip2long($ip);
if ($ipLong === false) return false;
foreach ($ranges as $cidr) {
[$subnet, $bits] = explode('/', $cidr);
$subnetLong = ip2long($subnet);
if ($subnetLong === false) continue;
$mask = -1 << (32 - (int)$bits);
if (($ipLong & $mask) === ($subnetLong & $mask)) {
return true;
}
}
return false;
}
/**
* التعرّف على IPs محركات البحث والبوتات المعروفة (بالـ IP مش الـ UA).
*
* ⚠️ في بوابة الدفع: نستخدم هذا للـ *حظر* مش السماح.
* ما بدنا الصفحة تنفهرس ولا أي بوت يوصلها.
*
* ⚠️ ليش بالـ IP؟ الـ UA سهل تزييفه. نطاقات الـ IP الرسمية أصعب.
* النطاقات بتتحدّث، راجعها من المصادر الرسمية كل فترة.
*/
private function isKnownBotIP(string $ip): bool
{
if (!filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
return false;
}
// نطاقات محركات البحث والزحف المعروفة
$crawlerRanges = [
// ---- Google (Googlebot + خدمات الزحف والفهرسة) ----
'66.249.64.0/19', // Googlebot الأساسي
'66.102.0.0/20', // Google
'72.14.192.0/18', // Google
'74.125.0.0/16', // Google
'108.177.0.0/17', // Google
'172.217.0.0/16', // Google
'173.194.0.0/16', // Google
'192.178.0.0/15', // Google
'209.85.128.0/17', // Google
'216.239.32.0/19', // Google
'142.250.0.0/15', // Google
'34.100.182.96/28', '34.101.50.144/28', // Google special crawlers
// ---- Bing (Bingbot) ----
'157.55.39.0/24', '207.46.13.0/24', '40.77.167.0/24',
'13.66.139.0/24', '13.66.144.0/24', '52.167.144.0/24',
'13.67.10.16/28', '13.69.66.240/28', '40.77.188.0/22',
'65.55.210.0/24', '199.30.24.0/23', '40.77.139.0/25',
// ---- DuckDuckGo ----
'20.191.45.212/32', '40.88.21.235/32', '40.76.173.151/32',
'40.76.163.7/32', '20.185.79.47/32', '52.142.26.175/32',
// ---- Yandex ----
'5.45.192.0/18', '5.255.192.0/18', '37.9.64.0/18',
'77.88.0.0/18', '87.250.224.0/19', '93.158.128.0/18',
'95.108.128.0/17', '141.8.128.0/18', '178.154.128.0/17',
// ---- Apple (Applebot) ----
'17.0.0.0/8',
];
$ipLong = ip2long($ip);
if ($ipLong === false) return false;
foreach ($crawlerRanges as $cidr) {
[$subnet, $bits] = explode('/', $cidr);
$subnetLong = ip2long($subnet);
if ($subnetLong === false) continue;
$mask = -1 << (32 - (int)$bits);
if (($ipLong & $mask) === ($subnetLong & $mask)) {
return true;
}
}
return false;
}
private function checkRateLimit(): bool
{
$file = $this->config['rate_limit_dir'] . md5($this->ip) . '.txt';
$now = time();
$minute = floor($now / 60);
$data = ['minute' => $minute, 'count' => 0];
if (is_file($file)) {
$stored = @json_decode(file_get_contents($file), true);
if ($stored && $stored['minute'] === $minute) {
$data = $stored;
}
}
$data['count']++;
@file_put_contents($file, json_encode($data), LOCK_EX);
return $data['count'] <= $this->config['rate_limit_per_min'];
}
private function isBlacklisted(): bool
{
$file = $this->config['rate_limit_dir'] . 'blacklist_' . md5($this->ip) . '.txt';
if (!is_file($file)) return false;
$until = (int)file_get_contents($file);
if ($until < time()) {
@unlink($file);
return false;
}
return true;
}
private function addToBlacklist(string $ip, string $reason, int $minutes = 60): void
{
$file = $this->config['rate_limit_dir'] . 'blacklist_' . md5($ip) . '.txt';
@file_put_contents($file, time() + ($minutes * 60));
$this->log('blacklist_add', ['ip' => $ip, 'reason' => $reason, 'duration_min' => $minutes]);
}
private function generateMathChallenge(): array
{
$ops = ['+', '-', '*'];
$op = $ops[array_rand($ops)];
$a = random_int(10, 99);
$b = random_int(2, 20);
switch ($op) {
case '+': $answer = $a + $b; break;
case '-': $answer = $a - $b; break;
case '*': $answer = $a * $b; break;
default: $answer = 0;
}
return compact('a', 'b', 'op', 'answer');
}
/**
* التحقق من توكن reCAPTCHA v3 مع جوجل
* بترجع: success, score (0.0-1.0), action, error
*/
private function verifyRecaptcha(string $token): array
{
if (empty($token)) {
return ['success' => false, 'error' => 'missing_token', 'score' => 0.0, 'action' => ''];
}
$ch = curl_init('https://www.google.com/recaptcha/api/siteverify');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_POSTFIELDS => http_build_query([
'secret' => $this->config['recaptcha_secret'],
'response' => $token,
'remoteip' => $this->ip,
]),
]);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
// فشل الاتصال بجوجل - لا تحظر بسبب مشكلة شبكة عندك
// score محايد 0.5 + success=false بعقوبة بسيطة (راجع الملاحظات)
return ['success' => false, 'error' => 'verify_unreachable', 'score' => 0.5, 'action' => ''];
}
$data = json_decode($response, true);
if (!($data['success'] ?? false)) {
$errors = implode(',', $data['error-codes'] ?? ['unknown']);
return ['success' => false, 'error' => $errors, 'score' => 0.0, 'action' => ''];
}
return [
'success' => true,
'score' => (float)($data['score'] ?? 0.0),
'action' => $data['action'] ?? '',
'error' => '',
];
}
private function log(string $event, array $data): void
{
$entry = json_encode([
'time' => date('c'),
'event' => $event,
'data' => $data,
], JSON_UNESCAPED_UNICODE) . "\n";
@file_put_contents($this->config['log_file'], $entry, FILE_APPEND | LOCK_EX);
}
}