diff --git a/lib/Cloak/FpRiskExplain.php b/lib/Cloak/FpRiskExplain.php index 48afe57..abdaad5 100644 --- a/lib/Cloak/FpRiskExplain.php +++ b/lib/Cloak/FpRiskExplain.php @@ -120,7 +120,7 @@ class FpRiskExplain private static function loadAnalyzer() { if (!self::$analyzerLoaded) { - require_once dirname(__DIR__, 2) . '/__code__/risk_analyzer_new.php'; + require_once __DIR__ . '/LogRiskAnalyzer.php'; self::$analyzerLoaded = true; } } @@ -132,7 +132,7 @@ class FpRiskExplain } self::loadAnalyzer(); try { - $analyzer = new RiskAnalyzer($hdata); + $analyzer = new LogRiskAnalyzer($hdata); return $analyzer->analyze(); } catch (Throwable $e) { return ['error' => $e->getMessage()]; @@ -417,8 +417,8 @@ class FpRiskExplain if ($localLevel !== null && $apiLevel !== null && abs($localLevel - $apiLevel) > 3) { $lines[] = [ 'level' => 'alert', - 'text' => sprintf('API reason 解析风险分 %d,本地 risk_analyzer 复算 %d,两者不一致。', $apiLevel, $localLevel), - 'action' => '线上 byApiRisk 版本可能与本地 __code__/risk_analyzer_new.php 不同步,或 reason 格式有变,需核对 API 部署。', + 'text' => sprintf('API reason 解析风险分 %d,日志复算风险分 %d,两者不一致。', $apiLevel, $localLevel), + 'action' => '线上 byApiRisk 版本可能与 lib/Cloak/LogRiskAnalyzer.php 不同步,或 reason 格式有变,需核对 API 部署。', ]; } diff --git a/lib/Cloak/LogRiskAnalyzer.php b/lib/Cloak/LogRiskAnalyzer.php new file mode 100644 index 0000000..779e9f6 --- /dev/null +++ b/lib/Cloak/LogRiskAnalyzer.php @@ -0,0 +1,1157 @@ += risk_number → 拦截 (result=false) + */ +class LogRiskAnalyzer { + public $countries; + public $countriesByContinent; + public $langLabels; + public $countryLangMap; + public $commonResolutions; + public $timezones; + public $countryTimezoneMap; + public $unqiePixels; + + private $riskLevel = 0; + private $riskFactors = []; + private $fingerprint; + private $fingerprintData; + /** @var bool 是否加强判断模式 */ + private $enhanced = false; + /** @var array 各类别已计分,用于封顶防叠加 */ + private $categoryScores = []; + + /** 各类别单次 analyze 内加分上限 */ + private const CATEGORY_CAPS = [ + 'ua_bot' => 40, + 'webgl_virtual' => 35, + 'webgl_params' => 15, + 'geo_lang_tz' => 15, + 'screen' => 35, + 'hardware' => 15, + 'hostname' => 25, + ]; + + public function __construct($data) { + $this->fingerprintData = $data; + $this->enhanced = $this->detectEnhancedMode($data); + } + + /** + * 优先读 hdata.risk_enhanced;其次根据 canvas/webgl/browser 是否齐全推断 + */ + private function detectEnhancedMode(array $data): bool { + if (!empty($data['risk_enhanced'])) { + return true; + } + return !empty($data['canvas']) && is_array($data['canvas']) + && !empty($data['webgl']) && is_array($data['webgl']) + && !empty($data['browser']) && is_array($data['browser']); + } + + /** + * 带类别封顶的加分 + */ + private function addRisk(string $category, int $points, string $reason, ?int $cap = null): void { + if ($points === 0) { + return; + } + $max = $cap ?? (self::CATEGORY_CAPS[$category] ?? 100); + $used = $this->categoryScores[$category] ?? 0; + + if ($points > 0) { + $room = max(0, $max - $used); + $applied = min($points, $room); + if ($applied <= 0) { + return; + } + $this->categoryScores[$category] = $used + $applied; + } else { + $applied = $points; + } + + $this->riskLevel += $applied; + $this->riskFactors[] = $reason; + } + + private function getScreenWidth(): int { + $screen = $this->fingerprintData['screen'] ?? null; + if (!is_array($screen)) { + return 0; + } + if (isset($screen['width'])) { + return (int) $screen['width']; + } + return (int) ($screen[0] ?? 0); + } + + private function hasTouchscreen(): bool { + $touch = $this->fingerprintData['touchscreen'] ?? [0, false, false]; + return is_array($touch) && ( + ($touch[0] ?? 0) > 0 || !empty($touch[1]) || !empty($touch[2]) + ); + } + + private function uaIndicatesTablet(string $ua): bool { + return (bool) preg_match('/iPad|Tablet|SM-T|Kindle|Silk\/|PlayBook|Tab(?:let)?\b/i', $ua); + } + + private function uaIndicatesMobile(string $ua): bool { + return (bool) preg_match( + '/iPhone|iPod|Android.*Mobile|Mobile\/|IEMobile|webOS|BlackBerry|Opera Mini|Mobile Safari/i', + $ua + ); + } + + private function platformIndicatesMobile(): bool { + $platform = (string) ($this->fingerprintData['platform'] ?? ''); + if ($platform === '') { + return false; + } + return (bool) preg_match('/iPhone|iPod|Android/i', $platform); + } + + private function platformIndicatesTablet(): bool { + $platform = (string) ($this->fingerprintData['platform'] ?? ''); + return $platform !== '' && (bool) preg_match('/iPad/i', $platform); + } + + /** + * 推断设备类型。 + * + * 注意:基础模式 browser=null;iOS「请求桌面网站」会发送 Mac 形态 UA, + * 此时不能仅依赖 UAParser/UA,还需结合 platform 与 touchscreen。 + */ + private function getDeviceType(): string { + $ua = (string) ($this->fingerprintData['userAgent'] ?? ''); + + if ($this->uaIndicatesTablet($ua) || $this->platformIndicatesTablet()) { + return 'tablet'; + } + if ($this->uaIndicatesMobile($ua) || $this->platformIndicatesMobile()) { + return 'mobile'; + } + + $parsedType = strtolower((string) ($this->fingerprintData['browser']['device']['type'] ?? '')); + if (in_array($parsedType, ['mobile', 'tablet', 'wearable', 'smarttv', 'console', 'embedded', 'xr'], true)) { + return $parsedType; + } + + // iOS 桌面模式:UA 形如 Macintosh,但 platform/touch/窄屏仍暴露为手机 + if ($this->hasTouchscreen()) { + $screenWidth = $this->getScreenWidth(); + if ($screenWidth > 0 && $screenWidth < 1024) { + return 'mobile'; + } + } + + if ($parsedType !== '') { + return $parsedType; + } + + return 'desktop'; + } + + /** + * 是否应对其应用「桌面环境」专属启发式(如无任务栏时 availScreen === screen)。 + * 移动端该等式恒成立,不能作为风险信号。 + */ + private function shouldApplyDesktopScreenHeuristics(): bool { + if ($this->isMobileDeviceType($this->getDeviceType())) { + return false; + } + if ($this->hasTouchscreen()) { + return false; + } + + $ua = (string) ($this->fingerprintData['userAgent'] ?? ''); + if ($this->uaIndicatesMobile($ua) || $this->uaIndicatesTablet($ua)) { + return false; + } + if ($this->platformIndicatesMobile() || $this->platformIndicatesTablet()) { + return false; + } + + return true; + } + + private function getOsName(): ?string { + $platform = (string) ($this->fingerprintData['platform'] ?? ''); + if (preg_match('/iPad|iPhone|iPod/i', $platform)) { + return 'iOS'; + } + + if (!empty($this->fingerprintData['browser']['os']['name'])) { + $os = (string) $this->fingerprintData['browser']['os']['name']; + if (preg_match('/macOS/i', $os) && ($this->platformIndicatesMobile() || $this->platformIndicatesTablet())) { + return 'iOS'; + } + return $os; + } + + $ua = (string) ($this->fingerprintData['userAgent'] ?? ''); + if (preg_match('/Windows/i', $ua)) { + return 'Windows'; + } + if (preg_match('/Android/i', $ua)) { + return 'Android'; + } + if (preg_match('/iPhone|iPad|iOS/i', $ua)) { + return 'iOS'; + } + if (preg_match('/Mac OS X|Macintosh/i', $ua)) { + if ($this->platformIndicatesMobile() || $this->platformIndicatesTablet() || $this->hasTouchscreen()) { + return 'iOS'; + } + return 'macOS'; + } + return null; + } + + private function isMobileDeviceType(string $deviceType): bool { + return in_array($deviceType, ['mobile', 'tablet'], true); + } + + // 执行完整风险评估 + public function analyze() { + // === 两种模式均执行:强自动化 / 轻量环境 === + $this->checkWebDriver(); + $this->checkUserAgent(); + $this->checkHardwareConcurrency(); + $this->checkDeviceMemory(); + $this->checkTouchscreenConsistency(); + $this->checkScreenResolution(); + $this->checkTimezoneAnomalies(); + $this->checkLanguages(); + + // === 仅加强模式:深度指纹 === + if ($this->enhanced) { + $this->hashFingerprint(); + $this->checkWebGLRenderer(); + $this->checkBrowserFeatures(); + $this->checkFonts(); + $this->checkUserAgentGpuConsistency(); + $this->checkTouchscreenGpuConsistency(); + } + + // 反向 DNS 慢且 CDN 误伤高,两种模式均降权且仅在 IP 存在时执行 + $this->checkHostname(); + + $this->riskLevel = min(100, max(0, $this->riskLevel)); + + return [ + 'fingerprint' => $this->fingerprint, + 'risk_level' => $this->riskLevel, + 'risk_factors' => $this->riskFactors, + 'risk_enhanced' => $this->enhanced, + 'risk_category' => $this->getRiskCategory(), + 'risk_description' => $this->getRiskDescription(), + ]; + } + + public function hashFingerprint() { + if (empty($this->fingerprintData['canvas']) || !is_array($this->fingerprintData['canvas'])) { + return; + } + if (($this->fingerprintData['canvas'][0] ?? '') === 'canvas winding:yes') { + $this->fingerprint = md5( + ($this->fingerprintData['canvas'][0] ?? '') . ($this->fingerprintData['canvas'][1] ?? '') + ); + } else { + $this->fingerprint = null; + $this->addRisk('canvas', 35, 'Canvas 指纹无效或 winding 异常'); + } + } + + private function checkWebDriver() { + if (!empty($this->fingerprintData['webdriver'])) { + // 55 分:在 risk_number=50~55 时均可单独拦截 + $this->addRisk('webdriver', 55, '检测到 WebDriver,通常与自动化工具相关', 55); + } + } + + private function checkUserAgent() { + if (empty($this->fingerprintData['userAgent'])) { + return; + } + + $userAgent = strtolower($this->fingerprintData['userAgent']); + $botIndicators = [ + 'webdriver', 'selenium', 'phantom', 'headless', 'chromedriver', 'geckodriver', + 'bot', 'crawl', 'spider', 'slurp', 'scrap', 'robot', + 'facebookexternalhit', 'adsbot-google', 'google-adwords-instant', + 'emulator', 'simulator', 'screenshot', 'monitoring', + ]; + + $matched = false; + foreach ($botIndicators as $indicator) { + if (stripos($userAgent, $indicator) !== false) { + $matched = true; + $this->addRisk('ua_bot', 30, "User Agent 包含自动化/爬虫特征: {$indicator}"); + break; + } + } + + if (!$matched && stripos($userAgent, 'semrush') !== false) { + $this->addRisk('ua_bot', 25, 'User Agent 包含 SEO 爬虫特征: semrush'); + } + + if (stripos($userAgent, 'headlesschrome') !== false) { + $this->addRisk('ua_bot', 40, '检测到无头 Chrome,通常用于自动化', 40); + } + + $browsers = ['Chrome', 'Firefox', 'Safari', 'Edge']; + $osList = ['Windows', 'Mac', 'Android', 'iOS', 'Linux']; + $browserDetected = false; + $osDetected = false; + + foreach ($browsers as $browser) { + if (stripos($this->fingerprintData['userAgent'], $browser) !== false) { + $browserDetected = true; + break; + } + } + foreach ($osList as $osName) { + if (stripos($this->fingerprintData['userAgent'], $osName) !== false) { + $osDetected = true; + break; + } + } + + if (!$browserDetected || !$osDetected) { + $this->addRisk('ua_bot', $this->enhanced ? 12 : 8, '异常 User Agent: 无法识别常见浏览器或操作系统'); + } + + if (isset($this->fingerprintData['automation']) && is_array($this->fingerprintData['automation'])) { + $auto = $this->fingerprintData['automation']; + if (!empty($auto['phantom']) || !empty($auto['selenium'])) { + $this->addRisk('automation', 35, '检测到 PhantomJS 或 Selenium 相关组件', 35); + } + if (strpos($userAgent, 'chrome') !== false && empty($auto['isChrome'])) { + $this->addRisk('automation', $this->enhanced ? 18 : 12, 'UA 声称 Chrome 但缺少 window.chrome 对象'); + } + } + } + + /** 加强模式:UA 解析 OS 与 GPU renderer 交叉验证 */ + private function checkUserAgentGpuConsistency() { + $renderer = (string) ($this->fingerprintData['renderer'] ?? ''); + $os = $this->getOsName(); + if ($renderer === '' || $os === null) { + return; + } + + if (preg_match('/Windows/i', $os)) { + if (stripos($renderer, 'apple') !== false) { + $this->addRisk('gpu_os', 15, 'Windows UA 但检测到 Apple GPU renderer,疑似伪造'); + } + } elseif (preg_match('/macOS/i', $os)) { + if (stripos($renderer, 'nvidia') !== false) { + $this->addRisk('gpu_os', 12, 'macOS UA 但检测到 NVIDIA GPU,高度不一致'); + } + if (stripos($renderer, 'direct3d') !== false) { + $this->addRisk('gpu_os', 15, 'macOS UA 但 renderer 含 Direct3D(Windows 特征)'); + } + } + } + + private function checkWebGLRenderer() { + if (empty($this->fingerprintData['renderer'])) { + return; + } + + $renderer = (string) $this->fingerprintData['renderer']; + $virtualRenderers = [ + 'llvmpipe', 'swiftshader', 'software renderer', 'virtualbox', 'vmware', + 'qemu', 'parallels', 'hyper-v', 'headless', 'mesa offscreen', + ]; + + foreach ($virtualRenderers as $virt) { + if (stripos($renderer, $virt) !== false) { + $this->addRisk('webgl_virtual', 35, "检测到虚拟/软件渲染器: {$renderer}"); + break; + } + } + + if (empty($this->fingerprintData['webgl']) || !is_array($this->fingerprintData['webgl'])) { + return; + } + + $webgl = $this->fingerprintData['webgl']; + $extensionsRaw = isset($webgl[1]) ? (string) $webgl[1] : ''; + $extensions = $extensionsRaw !== '' ? explode(';', $extensionsRaw) : []; + if (count($extensions) > 0 && count($extensions) < 28) { + $this->addRisk('webgl_params', 8, 'WebGL 扩展数量较少'); + } + + $lineWidthRangeStr = isset($webgl[2]) + ? str_replace('webgl aliased line width range:', '', (string) $webgl[2]) + : ''; + $lineWidthRange = $lineWidthRangeStr ? json_decode($lineWidthRangeStr, true) : [0, 0]; + if (is_array($lineWidthRange) && $lineWidthRange[0] == 1 && $lineWidthRange[1] == 1) { + $this->addRisk('webgl_params', 2, 'Aliased line width range 为 [1,1]'); + } + + $maxTexture = isset($webgl[15]) + ? (int) str_replace('webgl max texture size:', '', (string) $webgl[15]) + : 0; + if ($maxTexture > 0 && $maxTexture < 8192) { + $this->addRisk('webgl_params', 2, "MAX_TEXTURE_SIZE 偏低 ({$maxTexture})"); + } + + $maxFragment = isset($webgl[12]) + ? (int) str_replace('webgl max fragment uniform vectors:', '', (string) $webgl[12]) + : 0; + if ($maxFragment > 0 && $maxFragment < 1024) { + $this->addRisk('webgl_params', 2, "MAX_FRAGMENT_UNIFORM_VECTORS 偏低 ({$maxFragment})"); + } + + $maxVarying = isset($webgl[16]) + ? (int) str_replace('webgl max varying vectors:', '', (string) $webgl[16]) + : 0; + if ($maxVarying > 0 && $maxVarying < 16) { + $this->addRisk('webgl_params', 2, "MAX_VARYING_VECTORS 偏低 ({$maxVarying})"); + } + + $maxViewportStr = isset($webgl[20]) + ? str_replace('webgl max viewport dims:', '', (string) $webgl[20]) + : ''; + $maxViewport = $maxViewportStr ? json_decode($maxViewportStr, true) : [0, 0]; + if (is_array($maxViewport) && $maxViewport[0] > 0 && $maxViewport[0] < 8192) { + $this->addRisk('webgl_params', 2, 'MAX_VIEWPORT_DIMS 偏低'); + } + + $maxAniso = isset($webgl[9]) + ? (int) str_replace('webgl max anisotropy:', '', (string) $webgl[9]) + : 0; + if ($maxAniso > 0 && $maxAniso <= 2) { + $this->addRisk('webgl_params', 4, '各向异性过滤不支持或极弱'); + } + + $antialiasing = isset($webgl[5]) + ? str_replace('webgl antialiasing:', '', (string) $webgl[5]) + : ''; + if ($antialiasing !== '' && $antialiasing !== 'yes') { + $this->addRisk('webgl_params', 8, '请求抗锯齿但未启用,常见于软件渲染'); + } + } + + private function checkHostname() { + $ip = trim((string) ($this->fingerprintData['ip'] ?? '')); + if ($ip === '' || !filter_var($ip, FILTER_VALIDATE_IP)) { + return; + } + + $hostname = @gethostbyaddr($ip); + if (!$hostname || $hostname === $ip) { + return; + } + + if (preg_match('/\.googlebot\.com$/i', $hostname)) { + $this->addRisk('hostname', 25, '反向 DNS 指向 Google 爬虫'); + } elseif (preg_match('/\.facebook\.com$/i', $hostname)) { + $this->addRisk('hostname', 25, '反向 DNS 指向 Facebook 爬虫'); + } elseif (preg_match('/\.tiktok\.com$/i', $hostname)) { + $this->addRisk('hostname', 25, '反向 DNS 指向 TikTok 爬虫'); + } + } + + private function checkHardwareConcurrency() { + if (empty($this->fingerprintData['hardwareConcurrency'])) { + return; + } + + $cores = (int) $this->fingerprintData['hardwareConcurrency']; + if ($cores <= 1) { + $this->addRisk('hardware', $this->enhanced ? 12 : 8, "硬件并发数极低: {$cores} 核"); + } elseif ($cores === 2 && $this->enhanced) { + $this->addRisk('hardware', 5, "硬件并发数偏低: {$cores} 核"); + } + } + + private function checkDeviceMemory() { + if (!isset($this->fingerprintData['deviceMemory']) || $this->fingerprintData['deviceMemory'] === '') { + return; + } + + $memory = (float) $this->fingerprintData['deviceMemory']; + if ($memory > 0 && $memory < 2) { + $this->addRisk('hardware', $this->enhanced ? 8 : 5, "设备内存偏低: {$memory}GB"); + } + } + + private function checkTouchscreenConsistency() { + $deviceType = $this->getDeviceType(); + $isMobile = $this->isMobileDeviceType($deviceType); + + $touch = $this->fingerprintData['touchscreen'] ?? [0, false, false]; + $hasTouchscreen = (is_array($touch) && ( + ($touch[0] ?? 0) > 0 || !empty($touch[1]) || !empty($touch[2]) + )); + + if ($isMobile && !$hasTouchscreen) { + $this->addRisk('touch', $this->enhanced ? 18 : 12, '移动设备报告无触摸屏,可能是自动化环境'); + } + + if ($deviceType === 'desktop' && $hasTouchscreen) { + $screenWidth = $this->getScreenWidth(); + if ($screenWidth > 0 && $screenWidth < 1024) { + $this->addRisk('touch', 5, '桌面设备有触摸屏但分辨率异常低'); + } + } + + if ($isMobile && !empty($this->fingerprintData['isEmulators'])) { + $this->addRisk('touch', 15, '设备可能是仿真器(无加速/陀螺仪)'); + } + + if ($this->enhanced && $isMobile) { + $os = $this->getOsName(); + if ($os !== null && !preg_match('/Android|iOS/i', $os)) { + $this->addRisk('touch', 10, '移动端 UA 但 OS 非 Android/iOS'); + } + } + } + + private function checkTouchscreenGpuConsistency() { + if (empty($this->fingerprintData['renderer'])) { + return; + } + $deviceType = $this->getDeviceType(); + if (!$this->isMobileDeviceType($deviceType)) { + return; + } + if (preg_match('/(nvidia|geforce|radeon)/i', (string) $this->fingerprintData['renderer'])) { + $this->addRisk('gpu_os', 12, '移动端 UA 但检测到桌面级 GPU (NVIDIA/AMD)'); + } + } + + private function checkScreenResolution() { + if (empty($this->fingerprintData['screen']) || !is_array($this->fingerprintData['screen'])) { + return; + } + + $screenSize = $this->fingerprintData['screen']; + $screenWidth = (int) ($screenSize[0] ?? ($screenSize['width'] ?? 0)); + $screenHeight = (int) ($screenSize[1] ?? ($screenSize['height'] ?? 0)); + + $availScreenSize = $this->fingerprintData['availScreen'] ?? [0, 0]; + $availScreenWidth = (int) ($availScreenSize[0] ?? ($availScreenSize['width'] ?? 0)); + $availScreenHeight = (int) ($availScreenSize[1] ?? ($availScreenSize['height'] ?? 0)); + + if ($screenWidth <= 0 || $screenHeight <= 0 || $availScreenWidth <= 0 || $availScreenHeight <= 0) { + $this->addRisk('screen', 40, '检测到无效或零分辨率', 40); + return; + } + + if ($screenWidth < 300 || $screenHeight < 400) { + $this->addRisk('screen', 15, '分辨率宽或高过小'); + } + + // 仅真实桌面环境:手机/平板 screen 与 availScreen 相同是正常现象 + if ($this->enhanced && $this->shouldApplyDesktopScreenHeuristics()) { + if ($screenWidth === $availScreenWidth && $screenHeight === $availScreenHeight) { + $this->addRisk('screen', 10, '桌面可用分辨率与屏幕分辨率完全相同(常见于无界面自动化)'); + } + + $widthDiff = $screenWidth - $availScreenWidth; + $heightDiff = $screenHeight - $availScreenHeight; + if ($widthDiff < 10 && $heightDiff < 10 && $screenWidth > 1000) { + $this->addRisk('screen', 8, '桌面可用区域与屏幕尺寸差异过小'); + } + } + + $dpr = $this->fingerprintData['devicePixelRatio'] ?? null; + if ($dpr !== null && ($dpr < 0.5 || $dpr > 5)) { + $this->addRisk('screen', $this->enhanced ? 18 : 12, "异常设备像素比: {$dpr}"); + } + + if ( + isset($this->fingerprintData['scale'], $this->fingerprintData['visualViewportOffset']) + && is_array($this->fingerprintData['visualViewportOffset']) + ) { + if ($this->fingerprintData['scale'] != 1 || ($this->fingerprintData['visualViewportOffset'][0] ?? 0) != 0) { + $this->addRisk('screen', -8, '视口存在缩放或偏移,降低自动化嫌疑'); + } + } + } + + private function checkBrowserFeatures() { + if (!$this->enhanced) { + return; + } + + $deviceType = $this->getDeviceType(); + $webglSupported = !empty($this->fingerprintData['webgl']); + $isModernBrowser = preg_match( + '/chrome\/(8[0-9]|9[0-9]|1[0-9]{2})|firefox\/(7[5-9]|8[0-9]|9[0-9]|1[0-9]{2})/i', + (string) ($this->fingerprintData['userAgent'] ?? '') + ); + + if ($isModernBrowser && !$webglSupported) { + $this->addRisk('browser_feat', 10, '现代浏览器但不支持 WebGL,可能是受限环境'); + } + + $webrtcSupported = !empty($this->fingerprintData['experimental'][0]['isWebRTCSupported']); + if (!$webrtcSupported) { + $this->addRisk('browser_feat', 5, '不支持 WebRTC(弱信号,隐私浏览器亦可能)'); + } + + $webGPUSupported = !empty($this->fingerprintData['webGPU']); + if ($webGPUSupported && !empty($this->fingerprintData['webGpuData']) && is_array($this->fingerprintData['webGpuData'])) { + $webGPU = $this->fingerprintData['webGpuData']; + $desc = (string) ($webGPU['adapterInfo']['description'] ?? ''); + if ($desc !== '' && stripos($desc, 'SwiftShader') !== false) { + $this->addRisk('browser_feat', 45, 'WebGPU 检测到 SwiftShader 软件渲染', 45); + } elseif ($desc !== '' && preg_match('/(VMware|VirtualBox|Parallels)/i', $desc)) { + $this->addRisk('browser_feat', 30, 'WebGPU 检测到虚拟机 GPU', 30); + } + $duration = (float) ($webGPU['benchmark']['durationMs'] ?? -1); + if ($duration > 800) { + $this->addRisk('browser_feat', 25, 'WebGPU 基准测试耗时过长,疑似软件渲染'); + } + } elseif ($deviceType === 'desktop' && $webGPUSupported === false) { + $this->addRisk('browser_feat', 5, '桌面环境未暴露 WebGPU(弱信号)'); + } + } + + private function checkLanguages() { + if (empty($this->fingerprintData['languages']) || empty($this->fingerprintData['ciso'])) { + return; + } + + $supportLangs = $this->getSupportLang($this->fingerprintData['ciso']); + if (empty($supportLangs)) { + return; + } + + $intersect = !empty(array_intersect( + $supportLangs, + array_map('strtolower', (array) $this->fingerprintData['languages']) + )); + + if (!$intersect) { + $this->addRisk('geo_lang_tz', $this->enhanced ? 8 : 5, '浏览器语言与 IP 国家常见语言不匹配'); + } + } + + private function checkTimezoneAnomalies() { + if (empty($this->fingerprintData['timezone']) || !isset($this->fingerprintData['timezoneOffset'])) { + return; + } + + $offsetHours = ($this->fingerprintData['timezoneOffset'] / 60) * -1; + $expectedTimezones = $this->getTimezonesByOffset($offsetHours); + + $found = false; + foreach ($expectedTimezones as $tz) { + if (stripos($this->fingerprintData['timezone'], $tz) !== false) { + $found = true; + break; + } + } + + if (!$found && !empty($expectedTimezones)) { + $this->addRisk('geo_lang_tz', 4, '时区名称与 offset 不完全匹配'); + } + + $timezoneOffset = abs((int) $this->fingerprintData['timezoneOffset']); + if ($timezoneOffset % 60 !== 0 && $timezoneOffset % 60 !== 30 && $timezoneOffset % 60 !== 45) { + $this->addRisk('geo_lang_tz', 6, "异常时区偏移: {$timezoneOffset} 分钟"); + } + + if (!empty($this->fingerprintData['ciso'])) { + $supportTZ = $this->getSupportTZ($this->fingerprintData['ciso']); + if (!empty($supportTZ) && !in_array(strtolower($this->fingerprintData['timezone']), $supportTZ, true)) { + $this->addRisk('geo_lang_tz', $this->enhanced ? 8 : 5, '时区不属于 IP 所在国家常见时区'); + } + } + } + + // 根据时区偏移获取可能的时区 + private function getTimezonesByOffset($offset) { + $timezones = [ + '-12' => ['Pacific/Kwajalein'], + '-11' => ['Pacific/Midway', 'Pacific/Niue', 'Pacific/Pago_Pago'], + '-10' => ['Pacific/Honolulu', 'Pacific/Rarotonga', 'Pacific/Tahiti'], + '-9' => ['America/Anchorage', 'Pacific/Gambier'], + '-8.5' => ['Pacific/Marquesas'], + '-8' => ['America/Vancouver', 'America/Tijuana', 'America/Los_Angeles', 'Pacific/Pitcairn', 'America/Whitehorse'], + '-7' => [ 'America/Dawson_Creek', 'America/Denver', 'America/Phoenix', 'America/Edmonton', 'America/Hermosillo', 'America/Mazatlan', 'America/Yellowknife'], + '-6' => ['America/Chicago', 'America/Winnipeg', 'America/Belize', 'America/Mexico_City', 'America/Chihuahua', 'America/Costa_Rica', 'America/Regina', 'America/El_Salvador', 'America/Tegucigalpa', 'America/Managua', 'America/Guatemala', 'Pacific/Galapagos'], + '-5' => ['America/New_York', 'America/Bogota', 'America/Nassau', 'America/Lima', 'America/Jamaica', 'America/Iqaluit', 'America/Havana', 'America/Guayaquil', 'America/Cayman', 'America/Cancun', 'Pacific/Easter', 'America/Toronto', 'America/Rio_Branco', 'America/Port-au-Prince', 'America/Panama'], + '-4' => ['America/Curacao','America/Martinique','America/Manaus','America/La_Paz','America/Halifax','America/Guyana','America/Grand_Turk','America/Port_of_Spain','America/Santo_Domingo','America/Thule','America/Boa_Vista','America/Barbados','Atlantic/Bermuda','America/Puerto_Rico','America/Porto_Velho'], + '-3.5' => ['America/Caracas'], + '-3' => ['Atlantic/Azores', 'Antarctica/Palmer', 'America/Cuiaba', 'America/Santiago', 'America/Recife', 'America/Paramaribo', 'America/Montevideo', 'America/Miquelon', 'America/Maceio', 'America/Godthab', 'America/Fortaleza', 'America/Cayenne', 'America/Belem', 'America/Bahia', 'America/Argentina/Buenos_Aires', 'America/Araguaina', 'Atlantic/Stanley', 'America/Asuncion', 'America/Campo_Grande', 'Antarctica/Rothera'], + '-2' => ['America/Sao_Paulo', 'Atlantic/South_Georgia', 'America/St_Johns', 'America/Noronha'], + '-1' => ['Atlantic/Cape_Verde', 'Atlantic/Azores', 'America/Scoresbysund'], + '0' => ['UTC', 'Europe/London', 'Africa/Bissau', 'Etc/Greenwich', 'Atlantic/Reykjavik', 'Atlantic/Faroe', 'Atlantic/Canary', 'America/Danmarkshavn', 'Africa/Monrovia', 'Africa/El_Aaiun', 'Africa/Casablanca', 'Europe/Dublin', 'Africa/Accra', 'Africa/Abidjan', 'Europe/Lisbon'], + '1' => ['Europe/Paris', 'Africa/Lagos', 'Europe/Prague', 'Europe/Rome', 'Europe/Stockholm', 'Africa/Algiers', 'Europe/Vienna', 'Europe/Warsaw', 'Europe/Zurich', 'Africa/Ceuta', 'Africa/Ndjamena', 'Africa/Tunis', 'Europe/Tirane', 'Europe/Amsterdam', 'Europe/Andorra', 'Europe/Belgrade', 'Europe/Berlin', 'Europe/Brussels', 'Europe/Budapest', 'Europe/Copenhagen', 'Europe/Gibraltar', 'Europe/Luxembourg', 'Europe/Madrid', 'Europe/Malta', 'Europe/Monaco', 'Europe/Oslo'], + '2' => ['Europe/Istanbul', 'Africa/Cairo', 'Asia/Jerusalem', 'Europe/Vilnius', 'Europe/Tallinn', 'Africa/Johannesburg', 'Africa/Maputo', 'Africa/Tripoli', 'Asia/Amman', 'Asia/Beirut', 'Asia/Damascus', 'Asia/Gaza', 'Africa/Windhoek', 'Asia/Nicosia', 'Europe/Athens', 'Europe/Bucharest', 'Europe/Chisinau', 'Europe/Helsinki', 'Europe/Kaliningrad', 'Europe/Kiev', 'Europe/Riga', 'Europe/Sofia'], + '3' => ['Europe/Moscow', 'Asia/Riyadh', 'Europe/Minsk', 'Asia/Qatar', 'Asia/Baghdad', 'Antarctica/Syowa', 'Africa/Nairobi', 'Africa/Khartoum'], + '3.5' => ['Asia/Tehran'], + '4' => ['Asia/Dubai', 'Asia/Baku', 'Indian/Mauritius', 'Indian/Mahe', 'Europe/Samara', 'Indian/Reunion', 'Asia/Tbilisi', 'Asia/Yerevan'], + '4.5' => ['Asia/Kabul'], + '5'=> ['Asia/Karachi', 'Asia/Tashkent', 'Indian/Kerguelen', 'Asia/Yekaterinburg', 'Asia/Dushanbe', 'Asia/Ashgabat', 'Asia/Aqtobe', 'Asia/Aqtau', 'Indian/Maldives', 'Antarctica/Mawson'], + '5.5' => ['Asia/Calcutta', 'Asia/Colombo', 'Asia/Kolkata'], + '5.75' => ['Asia/Kathmandu'], + '6' => ['Indian/Chagos', 'Asia/Thimphu', 'Asia/Dhaka', 'Asia/Bishkek', 'Asia/Almaty', 'Antarctica/Vostok', 'Asia/Omsk'], + '6.5' => ['Asia/Rangoon', 'Indian/Cocos'], + '7' => ['Asia/Bangkok', 'Indian/Christmas', 'Asia/Saigon', 'Asia/Krasnoyarsk', 'Asia/Jakarta', 'Asia/Hovd', 'Antarctica/Davis'], + '8' => ['Asia/Choibalsan', 'Asia/Taipei', 'Asia/Singapore', 'Australia/Perth', 'Asia/Manila', 'Asia/Makassar', 'Asia/Macau', 'Asia/Kuala_Lumpur', 'Asia/Irkutsk', 'Asia/Hong_Kong', 'Asia/Ulaanbaatar', 'Asia/Brunei', 'Antarctica/Casey', 'Asia/Shanghai', 'Asia/Beijing'], + '8.5' => ['Asia/Pyongyang'], + '9' => ['Asia/Yakutsk', 'Asia/Tokyo', 'Asia/Seoul', 'Asia/Jayapura', 'Asia/Dili', 'Pacific/Palau'], + '9.5' => ['Australia/Darwin'], + '10' => ['Pacific/Guam', 'Pacific/Chuuk', 'Asia/Vladivostok', 'Asia/Magadan', 'Antarctica/DumontDUrville', 'Pacific/Port_Moresby', 'Australia/Brisbane'], + '10.5' => ['Australia/Adelaide'], + '11' => ['Australia/Sydney', 'Asia/Magadan', 'Pacific/Noumea', 'Pacific/Kosrae', 'Pacific/Guadalcanal', 'Pacific/Efate', 'Pacific/Pohnpei', 'Australia/Hobart', 'Pacific/Norfolk'], + '12' => ['Pacific/Funafuti', 'Pacific/Nauru', 'Pacific/Tarawa', 'Pacific/Wake', 'Asia/Kamchatka', 'Pacific/Kwajalein', 'Pacific/Majuro', 'Pacific/Wallis'], + '13' => ['Pacific/Enderbury', 'Pacific/Tongatapu', 'Pacific/Fakaofo', 'Pacific/Auckland', 'Pacific/Fiji'] + ]; + + return isset($timezones["$offset"]) ? $timezones["$offset"] : []; + } + + // 根据国家获取一般设置的语言 + private function getSupportLang($country_code) { + $browserLanguages = [ + "AF" => ["ps-af", "fa-af", "uz-af"], + "AL" => ["sq-al", "sq"], + "DZ" => ["ar-dz", "fr-dz", "ar", "fr"], + "AD" => ["ca-ad", "ca"], + "AO" => ["pt-ao", "pt"], + "AR" => ["es-ar", "es"], + "AM" => ["hy-am", "hy"], + "AU" => ["en-au", "en"], + "AT" => ["de-at", "de"], + "AZ" => ["az-latn-az", "az"], + "BH" => ["ar-bh", "ar"], + "BD" => ["bn-bd", "bn"], + "BY" => ["be-by", "ru-by", "be", "ru"], + "BE" => ["nl-be", "fr-be", "de-be", "nl", "fr", "de"], + "BZ" => ["en-bz", "es-bz", "en", "es"], + "BO" => ["es-bo", "qu-bo", "ay-bo", "es", "qu", "ay"], + "BA" => ["bs-ba", "hr-ba", "sr-ba", "bs", "hr", "sr"], + "BR" => ["pt-br", "pt"], + "BN" => ["ms-bn", "ms"], + "BG" => ["bg-bg", "bg"], + "KH" => ["km-kh", "km"], + "CA" => ["en-ca", "fr-ca", "en", "fr"], + "CL" => ["es-cl", "es"], + "CN" => ["zh-cn", "zh"], + "CO" => ["es-co", "es"], + "CR" => ["es-cr", "es"], + "HR" => ["hr-hr", "hr"], + "CU" => ["es-cu", "es"], + "CY" => ["el-cy", "tr-cy", "el", "tr"], + "CZ" => ["cs-cz", "cs"], + "DK" => ["da-dk", "da"], + "DO" => ["es-do", "es"], + "EC" => ["es-ec", "es"], + "EG" => ["ar-eg", "ar"], + "SV" => ["es-sv", "es"], + "EE" => ["et-ee", "et"], + "ET" => ["am-et", "om-et", "ti-et", "am", "om", "ti"], + "FI" => ["fi-fi", "sv-fi", "fi", "sv"], + "FR" => ["fr-fr", "fr"], + "GE" => ["ka-ge", "ka"], + "DE" => ["de-de", "de"], + "GR" => ["el-gr", "el"], + "GT" => ["es-gt", "es"], + "HN" => ["es-hn", "es"], + "HK" => ["zh-hk", "en-hk", "zh-tw", "zh", "en"], + "HU" => ["hu-hu", "hu"], + "IS" => ["is-is", "is"], + "IN" => ["hi-in", "en-in", "ta-in", "te-in", "bn-in", "hi", "en", "ta", "te", "bn"], + "ID" => ["id-id", "id"], + "IR" => ["fa-ir", "fa"], + "IQ" => ["ar-iq", "ku-iq", "ar", "ku"], + "IE" => ["en-ie", "ga-ie", "en", "ga"], + "IL" => ["he-il", "ar-il", "en-il", "he", "ar", "en"], + "IT" => ["it-it", "de-it", "it", "de"], + "JM" => ["en-jm", "en"], + "JP" => ["ja-jp", "ja"], + "JO" => ["ar-jo", "ar"], + "KZ" => ["kk-kz", "ru-kz", "kk", "ru"], + "KE" => ["sw-ke", "en-ke", "sw", "en"], + "KR" => ["ko-kr", "ko"], + "KW" => ["ar-kw", "ar"], + "LA" => ["lo-la", "lo"], + "LV" => ["lv-lv", "lv"], + "LB" => ["ar-lb", "fr-lb", "ar", "fr"], + "LI" => ["de-li", "de"], + "LT" => ["lt-lt", "lt"], + "LU" => ["fr-lu", "de-lu", "lb-lu", "fr", "de", "lb"], + "MO" => ["zh-mo", "pt-mo", "zh-hk", "zh", "pt"], + "MY" => ["ms-my", "en-my", "zh-my", "ms", "en", "zh"], + "MX" => ["es-mx", "es"], + "MD" => ["ro-md", "ru-md", "ro", "ru"], + "MC" => ["fr-mc", "it-mc", "fr", "it"], + "MN" => ["mn-mn", "mn"], + "MA" => ["ar-ma", "fr-ma", "ar", "fr"], + "NL" => ["nl-nl", "fy-nl", "nl", "fy"], + "NZ" => ["en-nz", "mi-nz", "en", "mi"], + "NI" => ["es-ni", "es"], + "NG" => ["en-ng", "ha-ng", "ig-ng", "yo-ng", "en", "ha", "ig", "yo"], + "NO" => ["nb-no", "nn-no", "se-no", "nb", "nn", "se"], + "OM" => ["ar-om", "ar"], + "PK" => ["ur-pk", "en-pk", "pa-pk", "ur", "en", "pa"], + "PA" => ["es-pa", "es"], + "PY" => ["es-py", "gn-py", "es", "gn"], + "PE" => ["es-pe", "qu-pe", "ay-pe", "es", "qu", "ay"], + "PH" => ["en-ph", "fil-ph", "en", "fil"], + "PL" => ["pl-pl", "pl"], + "PT" => ["pt-pt", "pt"], + "PR" => ["es-pr", "en-pr", "es", "en"], + "QA" => ["ar-qa", "ar"], + "RO" => ["ro-ro", "ro"], + "RU" => ["ru-ru", "tt-ru", "ru", "tt"], + "SA" => ["ar-sa", "ar"], + "RS" => ["sr-cyrl-rs", "sr-latn-rs", "sr"], + "SG" => ["en-sg", "zh-sg", "ms-sg", "ta-sg", "en", "zh", "ms", "ta"], + "SK" => ["sk-sk", "sk"], + "SI" => ["sl-si", "sl"], + "ZA" => ["en-za", "af-za", "zu-za", "xh-za", "en", "af", "zu", "xh"], + "ES" => ["es-es", "ca-es", "gl-es", "eu-es", "es", "ca", "gl", "eu"], + "LK" => ["si-lk", "ta-lk", "si", "ta"], + "SE" => ["sv-se", "sv"], + "CH" => ["de-ch", "fr-ch", "it-ch", "rm-ch", "de", "fr", "it", "rm"], + "SY" => ["ar-sy", "ar"], + "TW" => ["zh-tw", "zh"], + "TH" => ["th-th", "th"], + "TT" => ["en-tt", "en"], + "TN" => ["ar-tn", "fr-tn", "ar", "fr"], + "TR" => ["tr-tr", "tr"], + "UA" => ["uk-ua", "ru-ua", "uk", "ru"], + "AE" => ["ar-ae", "en-ae", "ar", "en"], + "GB" => ["en-gb", "cy-gb", "gd-gb", "en", "cy", "gd"], + "US" => ["en-us", "es-us", "en", "es"], + "UY" => ["es-uy", "es"], + "UZ" => ["uz-latn-uz", "ru-uz", "uz", "ru"], + "VE" => ["es-ve", "es"], + "VN" => ["vi-vn", "vi"], + "YE" => ["ar-ye", "ar"], + "ZW" => ["en-zw", "sn-zw", "nd-zw", "en", "sn", "nd"], + ]; + + return isset($browserLanguages[$country_code]) ? $browserLanguages[$country_code] : []; + } + + // 根据国家获取一般设置的时区 + private function getSupportTZ($country_code) { + $countryTimezoneMap = [ + 'AD' => ['Europe/Andorra'], + 'AE' => ['Asia/Dubai'], + 'AF' => ['Asia/Kabul'], + 'AG' => ['America/Antigua'], + 'AI' => ['America/Anguilla'], + 'AL' => ['Europe/Tirane'], + 'AM' => ['Asia/Yerevan'], + 'AO' => ['Africa/Luanda'], + 'AQ' => ['Antarctica/Casey', 'Antarctica/Davis', 'Antarctica/DumontDUrville', 'Antarctica/Macquarie', 'Antarctica/Mawson', 'Antarctica/McMurdo', 'Antarctica/Palmer', 'Antarctica/Rothera', 'Antarctica/South_Pole', 'Antarctica/Syowa', 'Antarctica/Troll', 'Antarctica/Vostok'], + 'AR' => ['America/Argentina/Buenos_Aires', 'America/Argentina/Catamarca', 'America/Argentina/ComodRivadavia', 'America/Argentina/Cordoba', 'America/Argentina/Jujuy', 'America/Argentina/La_Rioja', 'America/Argentina/Mendoza', 'America/Argentina/Rio_Gallegos', 'America/Argentina/Salta', 'America/Argentina/San_Juan', 'America/Argentina/San_Luis', 'America/Argentina/Tucuman', 'America/Argentina/Ushuaia', 'America/Buenos_Aires', 'America/Catamarca', 'America/Cordoba', 'America/Jujuy', 'America/Mendoza', 'America/Rosario'], + 'AS' => ['Pacific/Pago_Pago', 'Pacific/Samoa', 'US/Samoa'], + 'AT' => ['Europe/Vienna'], + 'AU' => ['Australia/ACT', 'Australia/Adelaide', 'Australia/Brisbane', 'Australia/Broken_Hill', 'Australia/Canberra', 'Australia/Currie', 'Australia/Darwin', 'Australia/Eucla', 'Australia/Hobart', 'Australia/LHI', 'Australia/Lindeman', 'Australia/Lord_Howe', 'Australia/Melbourne', 'Australia/NSW', 'Australia/North', 'Australia/Perth', 'Australia/Queensland', 'Australia/South', 'Australia/Sydney', 'Australia/Tasmania', 'Australia/Victoria', 'Australia/West', 'Australia/Yancowinna'], + 'AW' => ['America/Aruba'], + 'AX' => ['Europe/Mariehamn'], + 'AZ' => ['Asia/Baku'], + 'BA' => ['Europe/Sarajevo'], + 'BB' => ['America/Barbados'], + 'BD' => ['Asia/Dacca', 'Asia/Dhaka'], + 'BE' => ['Europe/Brussels'], + 'BF' => ['Africa/Ouagadougou'], + 'BG' => ['Europe/Sofia'], + 'BH' => ['Asia/Bahrain'], + 'BI' => ['Africa/Bujumbura'], + 'BJ' => ['Africa/Porto-Novo'], + 'BL' => ['America/St_Barthelemy'], + 'BM' => ['Atlantic/Bermuda'], + 'BN' => ['Asia/Brunei'], + 'BO' => ['America/La_Paz'], + 'BQ' => ['America/Kralendijk'], + 'BR' => ['America/Araguaina', 'America/Bahia', 'America/Belem', 'America/Boa_Vista', 'America/Campo_Grande', 'America/Cuiaba', 'America/Eirunepe', 'America/Fortaleza', 'America/Maceio', 'America/Manaus', 'America/Noronha', 'America/Porto_Acre', 'America/Porto_Velho', 'America/Recife', 'America/Rio_Branco', 'America/Santarem', 'America/Sao_Paulo', 'Brazil/Acre', 'Brazil/DeNoronha', 'Brazil/East', 'Brazil/West'], + 'BS' => ['America/Nassau'], + 'BT' => ['Asia/Thimphu'], + 'BW' => ['Africa/Gaborone'], + 'BY' => ['Europe/Minsk'], + 'BZ' => ['America/Belize'], + 'CA' => ['America/Atikokan', 'America/Blanc-Sablon', 'America/Cambridge_Bay', 'America/Coral_Harbour', 'America/Creston', 'America/Dawson', 'America/Dawson_Creek', 'America/Edmonton', 'America/Fort_Nelson', 'America/Glace_Bay', 'America/Goose_Bay', 'America/Halifax', 'America/Inuvik', 'America/Iqaluit', 'America/Moncton', 'America/Montreal', 'America/Nipigon', 'America/Pangnirtung', 'America/Rainy_River', 'America/Rankin_Inlet', 'America/Regina', 'America/Resolute', 'America/St_Johns', 'America/Swift_Current', 'America/Thunder_Bay', 'America/Toronto', 'America/Vancouver', 'America/Whitehorse', 'America/Winnipeg', 'America/Yellowknife', 'Canada/Atlantic', 'Canada/Central', 'Canada/Eastern', 'Canada/Mountain', 'Canada/Newfoundland', 'Canada/Pacific', 'Canada/Saskatchewan', 'Canada/Yukon'], + 'CC' => ['Indian/Cocos'], + 'CD' => ['Africa/Kinshasa', 'Africa/Lubumbashi'], + 'CF' => ['Africa/Bangui'], + 'CG' => ['Africa/Brazzaville'], + 'CH' => ['Europe/Zurich'], + 'CI' => ['Africa/Abidjan'], + 'CK' => ['Pacific/Rarotonga'], + 'CL' => ['America/Punta_Arenas', 'America/Santiago', 'Chile/Continental', 'Chile/EasterIsland', 'Pacific/Easter'], + 'CM' => ['Africa/Douala'], + 'CN' => ['Asia/Chongqing', 'Asia/Chungking', 'Asia/Harbin', 'Asia/Kashgar', 'Asia/Shanghai', 'Asia/Urumqi', 'PRC'], + 'CO' => ['America/Bogota'], + 'CR' => ['America/Costa_Rica'], + 'CU' => ['America/Havana', 'Cuba'], + 'CV' => ['Atlantic/Cape_Verde'], + 'CW' => ['America/Curacao'], + 'CX' => ['Indian/Christmas'], + 'CY' => ['Asia/Famagusta', 'Asia/Nicosia', 'Europe/Nicosia'], + 'CZ' => ['Europe/Prague'], + 'DE' => ['Europe/Berlin', 'Europe/Busingen'], + 'DJ' => ['Africa/Djibouti'], + 'DK' => ['Europe/Copenhagen'], + 'DM' => ['America/Dominica'], + 'DO' => ['America/Santo_Domingo'], + 'DZ' => ['Africa/Algiers'], + 'EC' => ['America/Guayaquil', 'Pacific/Galapagos'], + 'EE' => ['Europe/Tallinn'], + 'EG' => ['Africa/Cairo', 'Egypt'], + 'EH' => ['Africa/El_Aaiun'], + 'ER' => ['Africa/Asmara'], + 'ES' => ['Africa/Ceuta', 'Atlantic/Canary', 'Europe/Madrid'], + 'ET' => ['Africa/Addis_Ababa'], + 'FI' => ['Europe/Helsinki'], + 'FJ' => ['Pacific/Fiji'], + 'FK' => ['Atlantic/Stanley'], + 'FM' => ['Pacific/Chuuk', 'Pacific/Kosrae', 'Pacific/Pohnpei', 'Pacific/Ponape', 'Pacific/Truk', 'Pacific/Yap'], + 'FO' => ['Atlantic/Faeroe', 'Atlantic/Faroe'], + 'FR' => ['Europe/Paris'], + 'GA' => ['Africa/Libreville'], + 'GB' => ['Europe/Belfast', 'Europe/London', 'GB', 'GB-Eire'], + 'GD' => ['America/Grenada'], + 'GE' => ['Asia/Tbilisi'], + 'GF' => ['America/Cayenne'], + 'GG' => ['Europe/Guernsey'], + 'GH' => ['Africa/Accra'], + 'GI' => ['Europe/Gibraltar'], + 'GL' => ['America/Danmarkshavn', 'America/Godthab', 'America/Nuuk', 'America/Scoresbysund', 'America/Thule'], + 'GM' => ['Africa/Banjul'], + 'GN' => ['Africa/Conakry'], + 'GP' => ['America/Guadeloupe'], + 'GQ' => ['Africa/Malabo'], + 'GR' => ['Europe/Athens'], + 'GS' => ['Atlantic/South_Georgia'], + 'GT' => ['America/Guatemala'], + 'GU' => ['Pacific/Guam'], + 'GW' => ['Africa/Bissau'], + 'GY' => ['America/Guyana'], + 'HK' => ['Asia/Hong_Kong', 'Hongkong'], + 'HN' => ['America/Tegucigalpa'], + 'HR' => ['Europe/Zagreb'], + 'HT' => ['America/Port-au-Prince'], + 'HU' => ['Europe/Budapest'], + 'ID' => ['Asia/Jakarta', 'Asia/Jayapura', 'Asia/Makassar', 'Asia/Pontianak', 'Asia/Ujung_Pandang'], + 'IE' => ['Eire', 'Europe/Dublin'], + 'IL' => ['Asia/Jerusalem', 'Asia/Tel_Aviv', 'Israel'], + 'IM' => ['Europe/Isle_of_Man'], + 'IN' => ['Asia/Calcutta', 'Asia/Kolkata'], + 'IO' => ['Indian/Chagos'], + 'IQ' => ['Asia/Baghdad'], + 'IR' => ['Asia/Tehran', 'Iran'], + 'IS' => ['Atlantic/Reykjavik', 'Iceland'], + 'IT' => ['Europe/Rome'], + 'JE' => ['Europe/Jersey'], + 'JM' => ['America/Jamaica', 'Jamaica'], + 'JO' => ['Asia/Amman'], + 'JP' => ['Asia/Tokyo', 'Japan'], + 'KE' => ['Africa/Nairobi'], + 'KG' => ['Asia/Bishkek'], + 'KH' => ['Asia/Phnom_Penh'], + 'KI' => ['Pacific/Enderbury', 'Pacific/Kanton', 'Pacific/Kiritimati', 'Pacific/Tarawa'], + 'KM' => ['Indian/Comoro'], + 'KN' => ['America/St_Kitts'], + 'KP' => ['Asia/Pyongyang'], + 'KR' => ['Asia/Seoul', 'ROK'], + 'KW' => ['Asia/Kuwait'], + 'KY' => ['America/Cayman'], + 'KZ' => ['Asia/Almaty', 'Asia/Aqtau', 'Asia/Aqtobe', 'Asia/Atyrau', 'Asia/Oral', 'Asia/Qostanay', 'Asia/Qyzylorda'], + 'LA' => ['Asia/Vientiane'], + 'LB' => ['Asia/Beirut'], + 'LC' => ['America/St_Lucia'], + 'LI' => ['Europe/Vaduz'], + 'LK' => ['Asia/Colombo'], + 'LR' => ['Africa/Monrovia'], + 'LS' => ['Africa/Maseru'], + 'LT' => ['Europe/Vilnius'], + 'LU' => ['Europe/Luxembourg'], + 'LV' => ['Europe/Riga'], + 'LY' => ['Africa/Tripoli', 'Libya'], + 'MA' => ['Africa/Casablanca'], + 'MC' => ['Europe/Monaco'], + 'MD' => ['Europe/Chisinau', 'Europe/Tiraspol'], + 'ME' => ['Europe/Podgorica'], + 'MF' => ['America/Marigot'], + 'MG' => ['Indian/Antananarivo'], + 'MH' => ['Kwajalein', 'Pacific/Kwajalein', 'Pacific/Majuro'], + 'MK' => ['Europe/Skopje'], + 'ML' => ['Africa/Bamako', 'Africa/Timbuktu'], + 'MM' => ['Asia/Rangoon', 'Asia/Yangon'], + 'MN' => ['Asia/Choibalsan', 'Asia/Hovd', 'Asia/Ulaanbaatar', 'Asia/Ulan_Bator'], + 'MO' => ['Asia/Macao', 'Asia/Macau'], + 'MP' => ['Pacific/Saipan'], + 'MQ' => ['America/Martinique'], + 'MR' => ['Africa/Nouakchott'], + 'MS' => ['America/Montserrat'], + 'MT' => ['Europe/Malta'], + 'MU' => ['Indian/Mauritius'], + 'MV' => ['Indian/Maldives'], + 'MW' => ['Africa/Blantyre'], + 'MX' => ['America/Bahia_Banderas', 'America/Cancun', 'America/Chihuahua', 'America/Ciudad_Juarez', 'America/Ensenada', 'America/Hermosillo', 'America/Matamoros', 'America/Mazatlan', 'America/Merida', 'America/Mexico_City', 'America/Monterrey', 'America/Ojinaga', 'America/Santa_Isabel', 'America/Tijuana', 'Mexico/BajaNorte', 'Mexico/BajaSur', 'Mexico/General'], + 'MY' => ['Asia/Kuala_Lumpur', 'Asia/Kuching'], + 'MZ' => ['Africa/Maputo'], + 'NA' => ['Africa/Windhoek'], + 'NC' => ['Pacific/Noumea'], + 'NE' => ['Africa/Niamey'], + 'NF' => ['Pacific/Norfolk'], + 'NG' => ['Africa/Lagos'], + 'NI' => ['America/Managua'], + 'NL' => ['Europe/Amsterdam'], + 'NO' => ['Europe/Oslo'], + 'NP' => ['Asia/Kathmandu', 'Asia/Katmandu'], + 'NR' => ['Pacific/Nauru'], + 'NU' => ['Pacific/Niue'], + 'NZ' => ['Antarctica/McMurdo', 'Antarctica/South_Pole', 'NZ', 'NZ-CHAT', 'Pacific/Auckland', 'Pacific/Chatham'], + 'OM' => ['Asia/Muscat'], + 'PA' => ['America/Panama'], + 'PE' => ['America/Lima'], + 'PF' => ['Pacific/Gambier', 'Pacific/Marquesas', 'Pacific/Tahiti'], + 'PG' => ['Pacific/Bougainville', 'Pacific/Port_Moresby'], + 'PH' => ['Asia/Manila'], + 'PK' => ['Asia/Karachi'], + 'PL' => ['Europe/Warsaw', 'Poland'], + 'PM' => ['America/Miquelon'], + 'PN' => ['Pacific/Pitcairn'], + 'PR' => ['America/Puerto_Rico'], + 'PS' => ['Asia/Gaza', 'Asia/Hebron'], + 'PT' => ['Atlantic/Azores', 'Atlantic/Madeira', 'Europe/Lisbon', 'Portugal'], + 'PW' => ['Pacific/Palau'], + 'PY' => ['America/Asuncion'], + 'QA' => ['Asia/Qatar'], + 'RE' => ['Indian/Reunion'], + 'RO' => ['Europe/Bucharest'], + 'RS' => ['Europe/Belgrade'], + 'RU' => ['Asia/Anadyr', 'Asia/Barnaul', 'Asia/Chita', 'Asia/Irkutsk', 'Asia/Kamchatka', 'Asia/Khandyga', 'Asia/Krasnoyarsk', 'Asia/Magadan', 'Asia/Novokuznetsk', 'Asia/Novosibirsk', 'Asia/Omsk', 'Asia/Sakhalin', 'Asia/Srednekolymsk', 'Asia/Tomsk', 'Asia/Ust-Nera', 'Asia/Vladivostok', 'Asia/Yakutsk', 'Asia/Yekaterinburg', 'Europe/Astrakhan', 'Europe/Kaliningrad', 'Europe/Kirov', 'Europe/Moscow', 'Europe/Samara', 'Europe/Saratov', 'Europe/Ulyanovsk', 'Europe/Volgograd', 'W-SU'], + 'RW' => ['Africa/Kigali'], + 'SA' => ['Asia/Riyadh'], + 'SB' => ['Pacific/Guadalcanal'], + 'SC' => ['Indian/Mahe'], + 'SD' => ['Africa/Khartoum'], + 'SE' => ['Europe/Stockholm'], + 'SG' => ['Asia/Singapore', 'Singapore'], + 'SH' => ['Atlantic/St_Helena'], + 'SI' => ['Europe/Ljubljana'], + 'SJ' => ['Arctic/Longyearbyen', 'Atlantic/Jan_Mayen'], + 'SK' => ['Europe/Bratislava'], + 'SL' => ['Africa/Freetown'], + 'SM' => ['Europe/San_Marino'], + 'SN' => ['Africa/Dakar'], + 'SO' => ['Africa/Mogadishu'], + 'SR' => ['America/Paramaribo'], + 'SS' => ['Africa/Juba'], + 'ST' => ['Africa/Sao_Tome'], + 'SV' => ['America/El_Salvador'], + 'SX' => ['America/Lower_Princes'], + 'SY' => ['Asia/Damascus'], + 'SZ' => ['Africa/Mbabane'], + 'TC' => ['America/Grand_Turk'], + 'TD' => ['Africa/Ndjamena'], + 'TF' => ['Indian/Kerguelen'], + 'TG' => ['Africa/Lome'], + 'TH' => ['Asia/Bangkok'], + 'TJ' => ['Asia/Dushanbe'], + 'TK' => ['Pacific/Fakaofo'], + 'TL' => ['Asia/Dili'], + 'TM' => ['Asia/Ashgabat', 'Asia/Ashkhabad'], + 'TN' => ['Africa/Tunis'], + 'TO' => ['Pacific/Tongatapu'], + 'TR' => ['Asia/Istanbul', 'Europe/Istanbul', 'Turkey'], + 'TT' => ['America/Port_of_Spain'], + 'TV' => ['Pacific/Funafuti'], + 'TW' => ['Asia/Taipei', 'ROC'], + 'TZ' => ['Africa/Dar_es_Salaam'], + 'UA' => ['Europe/Kiev', 'Europe/Kyiv', 'Europe/Simferopol', 'Europe/Uzhgorod', 'Europe/Zaporozhye'], + 'UG' => ['Africa/Kampala'], + 'UM' => ['Pacific/Johnston', 'Pacific/Midway', 'Pacific/Wake'], + 'US' => ['America/Adak', 'America/Anchorage', 'America/Atka', 'America/Boise', 'America/Chicago', 'America/Denver', 'America/Detroit', 'America/Fort_Wayne', 'America/Indiana/Indianapolis', 'America/Indiana/Knox', 'America/Indiana/Marengo', 'America/Indiana/Petersburg', 'America/Indiana/Tell_City', 'America/Indiana/Vevay', 'America/Indiana/Vincennes', 'America/Indiana/Winamac', 'America/Indianapolis', 'America/Juneau', 'America/Kentucky/Louisville', 'America/Kentucky/Monticello', 'America/Knox_IN', 'America/Los_Angeles', 'America/Louisville', 'America/Menominee', 'America/Metlakatla', 'America/New_York', 'America/Nome', 'America/North_Dakota/Beulah', 'America/North_Dakota/Center', 'America/North_Dakota/New_Salem', 'America/Phoenix', 'America/Sitka', 'America/Yakutat', 'CST6CDT', 'EST5EDT', 'Navajo', 'PST8PDT', 'US/Alaska', 'US/Aleutian', 'US/Arizona', 'US/Central', 'US/East-Indiana', 'US/Eastern', 'US/Hawaii', 'US/Indiana-Starke', 'US/Michigan', 'US/Mountain', 'US/Pacific'], + 'UY' => ['America/Montevideo'], + 'UZ' => ['Asia/Samarkand', 'Asia/Tashkent'], + 'VA' => ['Europe/Vatican'], + 'VC' => ['America/St_Vincent'], + 'VE' => ['America/Caracas'], + 'VG' => ['America/Tortola', 'America/Virgin'], + 'VI' => ['America/St_Thomas'], + 'VN' => ['Asia/Ho_Chi_Minh', 'Asia/Saigon'], + 'VU' => ['Pacific/Efate'], + 'WF' => ['Pacific/Wallis'], + 'WS' => ['Pacific/Apia'], + 'YE' => ['Asia/Aden'], + 'YT' => ['Indian/Mayotte'], + 'ZA' => ['Africa/Johannesburg'], + 'ZM' => ['Africa/Lusaka'], + 'ZW' => ['Africa/Harare'], + ]; + + return isset($countryTimezoneMap[$country_code]) ? array_map('strtolower', $countryTimezoneMap[$country_code]) : []; + } + + private function checkFonts() { + if (!$this->enhanced) { + return; + } + + $fonts = !empty($this->fingerprintData['fonts']) && is_array($this->fingerprintData['fonts']) + ? $this->fingerprintData['fonts'] + : []; + if (empty($fonts)) { + return; + } + + $os = $this->getOsName(); + if ($os === null) { + return; + } + + if (preg_match('/Windows/i', $os)) { + if (!in_array('Calibri', $fonts, true) && !in_array('Segoe UI', $fonts, true)) { + $this->addRisk('fonts', 15, 'Windows UA 但缺少常见字体 Calibri/Segoe UI'); + } + if (in_array('San Francisco', $fonts, true)) { + $this->addRisk('fonts', 15, 'Windows UA 但检测到 macOS 字体 San Francisco'); + } + } elseif (preg_match('/macOS/i', $os)) { + if (!in_array('Helvetica Neue', $fonts, true) && !in_array('San Francisco', $fonts, true)) { + $this->addRisk('fonts', 15, 'macOS UA 但缺少常见 macOS 字体'); + } + if (in_array('Calibri', $fonts, true)) { + $this->addRisk('fonts', 15, 'macOS UA 但检测到 Windows 字体 Calibri'); + } + } + } + + private function getRiskCategory() { + if ($this->riskLevel < 20) { + return '低风险'; + } elseif ($this->riskLevel < 50) { + return '中等风险'; + } + return '高风险'; + } + + private function getRiskDescription() { + switch ($this->getRiskCategory()) { + case '低风险': + return '看起来是正常的用户访问,未检测到明显的自动化工具特征'; + case '中等风险': + return '检测到一些可能与自动化工具相关的特征,建议进一步观察'; + case '高风险': + return '很可能是自动化工具、爬虫或恶意访问,建议采取限制措施'; + default: + return '无法确定风险等级'; + } + } +} diff --git a/lib/DbHelper.php b/lib/DbHelper.php index d902d55..af910f1 100755 --- a/lib/DbHelper.php +++ b/lib/DbHelper.php @@ -40,7 +40,7 @@ if (!function_exists('cloak_visitor_log_format_timing')) { return ''; } if (!class_exists('CloakPipelineTimer', false)) { - require_once dirname(__DIR__) . '/Cloak/PipelineTimer.php'; + require_once __DIR__ . '/Cloak/PipelineTimer.php'; } $decoded = json_decode($raw, true); if (is_array($decoded)) { @@ -99,7 +99,7 @@ if (!function_exists('cloak_visitor_log_fp_risk_explain')) { function cloak_visitor_log_fp_risk_explain($fpHdata, $reason, $result, $domain = '') { if (!class_exists('FpRiskExplain', false)) { - require_once dirname(__DIR__) . '/Cloak/FpRiskExplain.php'; + require_once __DIR__ . '/Cloak/FpRiskExplain.php'; } return FpRiskExplain::build($fpHdata, $reason, $result, $domain); } diff --git a/lib_FLTSX.zip b/lib_FLTSX.zip new file mode 100755 index 0000000..2085555 Binary files /dev/null and b/lib_FLTSX.zip differ