false, 'message' => $context['error']]; } return self::execute($context, null); } /** * @param int $logId * @return array */ public static function replayFromLog($logId) { $row = self::fetchLogRow((int) $logId); if ($row === null) { return ['ok' => false, 'message' => '日志不存在']; } $context = self::contextFromLogRow($row); if (!empty($context['error'])) { return ['ok' => false, 'message' => $context['error']]; } return self::execute($context, (int) $logId); } /** * @param int $logId * @return array */ public static function prefillFromLog($logId) { $row = self::fetchLogRow((int) $logId); if ($row === null) { return ['ok' => false, 'message' => '日志不存在']; } $context = self::contextFromLogRow($row); if (!empty($context['error'])) { return ['ok' => false, 'message' => $context['error']]; } return [ 'ok' => true, 'log_id' => (int) $logId, 'prefill' => [ 'config_name' => $context['config_name'], 'ip' => $context['ip'], 'domain' => $context['domain'], 'page_url' => $context['page_url'], 'referer' => $context['referer'], 'user_agent' => $context['user_agent'], 'accept_language' => $context['accept_language'], 'visit_md5_code' => $context['visit_md5_code'], 'cookies_json' => $context['cookies_json'], 'fp_hdata' => $context['fp_hdata_raw'], 'device' => $context['device_override'], ], ]; } /** * @param string $reason * @return string */ public static function tagReason($reason) { $reason = trim((string) $reason); if ($reason === '') { return self::REASON_PREFIX; } if (strpos($reason, self::REASON_PREFIX) === 0) { return $reason; } return self::REASON_PREFIX . ' ' . $reason; } /** * @param array $input * @return array */ private static function normalizeManualInput(array $input) { $configName = ConfigLoader::sanitizeConfigName($input['config_name'] ?? ''); if (trim((string) ($input['config_name'] ?? '')) === '') { return ['error' => '请选择配置']; } $domain = self::normalizeHost($input['domain'] ?? ''); if ($domain === '') { $domain = self::defaultHostForConfig($configName); } if ($domain === '') { return ['error' => '请填写域名或绑定站点域名']; } $pageUrl = trim((string) ($input['page_url'] ?? '')); if ($pageUrl === '') { $pageUrl = 'https://' . $domain . '/' . $configName . '.php'; } $cookies = []; if (!empty($input['cookies_json'])) { $decoded = json_decode((string) $input['cookies_json'], true); if (!is_array($decoded)) { return ['error' => 'Cookie JSON 格式无效']; } $cookies = $decoded; } $fpRaw = trim((string) ($input['fp_hdata'] ?? '')); return [ 'config_name' => $configName, 'ip' => trim((string) ($input['ip'] ?? '8.8.8.8')), 'domain' => $domain, 'page_url' => $pageUrl, 'referer' => trim((string) ($input['referer'] ?? '')), 'user_agent' => trim((string) ($input['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')), 'accept_language' => trim((string) ($input['accept_language'] ?? 'en-US,en;q=0.9')), 'visit_md5_code' => trim((string) ($input['visit_md5_code'] ?? '')), 'cookies' => $cookies, 'cookies_json' => (string) ($input['cookies_json'] ?? ''), 'fp_hdata_raw' => $fpRaw, 'fp_hdata' => self::parseFpHdata($fpRaw), 'device_override' => trim((string) ($input['device'] ?? '')), ]; } /** * @param array $row * @return array */ private static function contextFromLogRow(array $row) { $domain = self::normalizeHost($row['domain'] ?? ''); $pageUrl = trim((string) ($row['page'] ?? '')); $config = self::guessConfigName($pageUrl, $domain); if ($config === '' && $domain !== '') { try { $pdo = DomainRepository::pdo(); DomainRepository::ensureTable($pdo); $domRow = DomainRepository::findByHostname($pdo, $domain); if ($domRow && !empty($domRow['config_name'])) { $config = trim($domRow['config_name']); } } catch (Throwable $e) { } } if ($config === '') { $config = 'index'; } $referer = trim((string) ($row['http_referer'] ?? '')); if ($referer === '') { $referer = trim((string) ($row['referer'] ?? '')); } $visitCode = trim((string) ($row['visit_md5_code'] ?? '')); $cookies = []; if ($visitCode !== '' && $visitCode !== 'loss') { $cookies['gfuid'] = $visitCode; } $fpRaw = trim((string) ($row['fp_hdata'] ?? '')); return [ 'config_name' => $config, 'ip' => trim((string) ($row['IP'] ?? '')), 'domain' => $domain !== '' ? $domain : self::defaultHostForConfig($config), 'page_url' => $pageUrl !== '' ? $pageUrl : ('https://' . ($domain ?: 'localhost') . '/' . $config . '.php'), 'referer' => $referer, 'user_agent' => trim((string) ($row['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')), 'accept_language' => trim((string) ($row['accept_language_raw'] ?? $row['language'] ?? 'en-US,en;q=0.9')), 'visit_md5_code' => $visitCode, 'cookies' => $cookies, 'cookies_json' => $cookies !== [] ? json_encode($cookies, JSON_UNESCAPED_UNICODE) : '', 'fp_hdata_raw' => $fpRaw, 'fp_hdata' => self::parseFpHdata($fpRaw), 'device_override' => trim((string) ($row['device'] ?? '')), ]; } /** * @param array $context * @param int|null $sourceLogId * @return array */ private static function execute(array $context, $sourceLogId) { if (!defined('DB_USERNAME')) { require_once dirname(__DIR__, 2) . '/cong.php'; } require_once dirname(__DIR__, 2) . '/lib/bootstrap.php'; $backup = self::backupGlobals(); try { if (session_status() !== PHP_SESSION_ACTIVE) { session_start(); } $_SESSION = []; self::injectEnvironment($context); $configName = ConfigLoader::loadByName($context['config_name']); $GLOBALS['__cloak_simulation_on'] = true; $GLOBALS['__cloak_force_sync_log'] = true; $GLOBALS['__cloak_debug_on'] = false; CloakPipelineTimer::init(); global $v_info, $reason, $config_name; $config_name = $configName; $v_info = new visitorInfo(); $v_info->get_visitor_Info(COSTM_IP_SCORE, CHECK_KEY, SHOW_SITE_COUNTRY); $reason = ''; CheckPipeline::run(); if (!isset($_SESSION['check_result']) || $_SESSION['check_result'] === '') { $_SESSION['check_result'] = 'false'; if ($reason === '') { $reason = '未产生判定结果,默认安全页'; } } $reason = cloak_finalize_reason($reason, $_SESSION['check_result']); $model = self::detectDevice($context); $logs = self::buildLogs($v_info, $reason, $model, $context, $sourceLogId); $route = SimulationRouteResolver::dispatch([ 'v_info' => $v_info, 'logs' => $logs, 'config_name' => $configName, 'reason' => $reason, 'model' => $model, ]); $secondary = null; if (!empty($route['needs_secondary'])) { if (!empty($context['fp_hdata']) && is_array($context['fp_hdata'])) { $secondary = self::runSecondary((int) ($route['log_id'] ?? 0), $context); } else { $secondary = [ 'skipped' => true, 'message' => '命中二次风控但未提供 fp_hdata,日志保持 wait 状态', ]; } } $timing = CloakPipelineTimer::finish(); return [ 'ok' => true, 'new_log_id' => $route['log_id'] ?? null, 'sim_source' => $sourceLogId, 'primary' => [ 'result' => $_SESSION['check_result'] ?? '', 'reason' => $reason, 'branch' => $route['branch'] ?? '', ], 'secondary' => $secondary, 'timing' => $timing, ]; } catch (Throwable $e) { return [ 'ok' => false, 'message' => $e->getMessage(), ]; } finally { self::restoreGlobals($backup); } } /** * @param int $logId * @param array $context * @return array|null */ private static function runSecondary($logId, array $context) { if ($logId <= 0) { return ['ok' => false, 'message' => '无效 log_id,无法执行二次风控']; } $fp = $context['fp_hdata']; if (!is_array($fp)) { return ['ok' => false, 'message' => 'fp_hdata 无效']; } $scheme = 'https://'; $host = $context['domain']; $fp['log_id'] = $logId; $fp['ip'] = $context['ip']; $fp['referer'] = $context['referer']; if (empty($fp['domain'])) { $fp['domain'] = $scheme . $host . '/'; } $result = FCheckHandler::process($fp); if (empty($result['ok'])) { return [ 'ok' => false, 'message' => $result['message'] ?? '二次风控失败', ]; } if (!empty($GLOBALS['__cloak_simulation_on']) && isset($result['reason'])) { self::tagReasonOnLog($logId, (string) $result['reason']); } return [ 'ok' => true, 'result' => !empty($result['result']), 'reason' => $result['reason'] ?? '', 'link' => $result['link'] ?? '', ]; } /** * @param int $logId * @param string $reason * @param bool $passed */ private static function tagReasonOnLog($logId, $reason) { $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); if (!$conn || $conn->connect_error) { return; } $reasonEsc = cloak_db_escape($conn, self::tagReason($reason)); $conn->query("UPDATE `visitor_logs` SET `reason`='{$reasonEsc}' WHERE `id`=" . (int) $logId); $conn->close(); } /** * @param array $context */ private static function injectEnvironment(array $context) { $host = $context['domain']; $pageUrl = $context['page_url']; $parts = parse_url($pageUrl); $path = $parts['path'] ?? ('/' . $context['config_name'] . '.php'); $query = $parts['query'] ?? ''; $_GET = []; if ($query !== '') { parse_str($query, $_GET); } $_REQUEST = $_GET; $_COOKIE = is_array($context['cookies']) ? $context['cookies'] : []; $visitCode = $context['visit_md5_code']; if ($visitCode === '' || $visitCode === 'loss') { $visitCode = md5($context['ip'] . microtime(true)); } $_COOKIE['gfuid'] = $visitCode; $_SESSION['gcuid'] = $visitCode; $_SESSION['gcu_ip'] = $context['ip']; $_SERVER['HTTP_HOST'] = $host; $_SERVER['SERVER_NAME'] = $host; $_SERVER['REMOTE_ADDR'] = $context['ip']; $_SERVER['HTTP_USER_AGENT'] = $context['user_agent']; $_SERVER['HTTP_REFERER'] = $context['referer']; $_SERVER['HTTP_ACCEPT_LANGUAGE'] = $context['accept_language']; $_SERVER['HTTPS'] = 'on'; $_SERVER['SERVER_PORT'] = '443'; $_SERVER['REQUEST_URI'] = $path . ($query !== '' ? '?' . $query : ''); $_SERVER['QUERY_STRING'] = $query; $_SERVER['PHP_SELF'] = $path; $_SERVER['SCRIPT_NAME'] = $path; } /** * @param visitorInfo $v_info * @param string $reason * @param string $model * @param array $context * @param int|null $sourceLogId * @return array */ private static function buildLogs($v_info, $reason, $model, array $context, $sourceLogId) { $logs = [ 'site_name' => str_replace('www.', '', $context['domain']), 'customers_ip' => $v_info->v_ip, 'country' => substr((string) $v_info->v_country, 0, 2), 'v_Browser' => $v_info->v_Browser, 'v_referer' => $v_info->v_referer, 'Client' => $v_info->v_Client, 'v_PageURL' => $v_info->v_curPageURL, 'accept_language' => $v_info->accept_language, 'visit_md5_code' => $_SESSION['gcuid'] ?? $v_info->visit_md5_code, 'campagin_id' => $v_info->costm_ip_score, 'result' => $_SESSION['check_result'] ?? 'false', 'device' => $model, 'reason' => self::tagReason($reason), 'user_agent' => $context['user_agent'], 'http_referer' => $context['referer'], 'accept_language_raw' => $context['accept_language'], 'is_simulation' => 1, ]; if ($sourceLogId) { $logs['sim_source_log_id'] = (int) $sourceLogId; } return $logs; } /** * @param array $context * @return string */ private static function detectDevice(array $context) { if ($context['device_override'] !== '') { return $context['device_override']; } $uaPath = dirname(__DIR__, 2) . '/Mobile-Detect/src/MobileDetect.php'; if (!is_file($uaPath)) { return 'PC'; } require_once $uaPath; $detect = new \Detection\MobileDetect; $deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer'); if ($deviceType === 'computer') { return 'PC'; } $patterns = $deviceType === 'tablet' ? $detect::getTabletDevices() : $detect::getPhoneDevices(); $model = ucfirst($deviceType); foreach ($patterns as $brand => $pattern) { if (preg_match('/' . $pattern . '/i', $context['user_agent'], $matches)) { $model = $brand . ' ' . (isset($matches[1]) ? $matches[1] : ''); break; } } return trim($model) !== '' ? trim($model) : 'Mobile'; } /** * @param string $raw * @return array|null */ private static function parseFpHdata($raw) { $raw = trim((string) $raw); if ($raw === '') { return null; } if ($raw[0] === '{' || $raw[0] === '[') { $decoded = json_decode($raw, true); return is_array($decoded) ? $decoded : null; } $decodedString = base64_decode($raw, true); if ($decodedString === false) { return null; } $decoded = json_decode($decodedString, true); return is_array($decoded) ? $decoded : null; } /** * @param string $pageUrl * @param string $domain * @return string */ private static function guessConfigName($pageUrl, $domain) { $path = parse_url($pageUrl, PHP_URL_PATH); if (is_string($path) && $path !== '') { $base = basename($path, '.php'); if ($base !== '' && preg_match('/^[a-zA-Z0-9_-]+$/', $base)) { return $base; } } return ''; } /** * @param string $configName * @return string */ private static function defaultHostForConfig($configName) { try { $pdo = DomainRepository::pdo(); DomainRepository::ensureTable($pdo); $host = DomainRepository::primaryHostnameForConfig($pdo, $configName); if ($host !== '') { return self::normalizeHost($host); } } catch (Throwable $e) { } return self::normalizeHost($_SERVER['HTTP_HOST'] ?? 'localhost'); } /** * @param string $host * @return string */ private static function normalizeHost($host) { $host = trim(strtolower((string) $host)); $host = preg_replace('#^https?://#', '', $host); $host = explode('/', $host)[0]; return preg_replace('/^www\./', '', $host); } /** * @param int $logId * @return array|null */ private static function fetchLogRow($logId) { if ($logId <= 0) { return null; } $conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); if (!$conn || $conn->connect_error) { return null; } VisitorLogSchema::ensureColumns($conn); $sql = 'SELECT * FROM `visitor_logs` WHERE `id`=' . (int) $logId . ' LIMIT 1'; $res = $conn->query($sql); if (!$res || $res->num_rows === 0) { $conn->close(); return null; } $row = $res->fetch_assoc(); $res->free(); $conn->close(); return $row; } /** * @return array */ private static function backupGlobals() { return [ '_SERVER' => $_SERVER, '_GET' => $_GET, '_REQUEST' => $_REQUEST, '_COOKIE' => $_COOKIE, '_SESSION' => $_SESSION, 'GLOBALS' => [ '__cloak_simulation_on' => $GLOBALS['__cloak_simulation_on'] ?? null, '__cloak_force_sync_log' => $GLOBALS['__cloak_force_sync_log'] ?? null, '__cloak_debug_on' => $GLOBALS['__cloak_debug_on'] ?? null, ], ]; } /** * @param array $backup */ private static function restoreGlobals(array $backup) { $_SERVER = $backup['_SERVER']; $_GET = $backup['_GET']; $_REQUEST = $backup['_REQUEST']; $_COOKIE = $backup['_COOKIE']; $_SESSION = $backup['_SESSION']; foreach ($backup['GLOBALS'] as $key => $value) { if ($value === null) { unset($GLOBALS[$key]); } else { $GLOBALS[$key] = $value; } } } }