修复日志页面卡顿我呢提

This commit is contained in:
root
2026-06-15 22:42:59 +08:00
parent 1a52201ec7
commit bcb9f293a6
151 changed files with 2461 additions and 471 deletions
Regular → Executable
View File
+137
View File
@@ -0,0 +1,137 @@
<?php
/**
* Session 与 f_check 跨域(CORS)辅助
*/
class CloakSession
{
public static function start()
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$domain = function_exists('cloak_fingerprint_cookie_domain')
? cloak_fingerprint_cookie_domain()
: '';
if ($domain !== '' && PHP_VERSION_ID >= 70300) {
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $domain,
'secure' => function_exists('cloak_request_is_https') && cloak_request_is_https(),
'httponly' => true,
'samesite' => 'Lax',
]);
} elseif ($domain !== '') {
session_set_cookie_params(
0,
'/',
$domain,
function_exists('cloak_request_is_https') && cloak_request_is_https(),
true
);
}
session_start();
}
}
class CloakFcheckCors
{
/**
* OPTIONS 预检:在 session/bootstrap 之前调用。
*/
public static function handlePreflight()
{
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'OPTIONS') {
return;
}
self::emitHeaders();
http_response_code(204);
exit;
}
/**
* 响应头(含 AJAX 跨子域 withCredentials
*/
public static function emitHeaders()
{
$origin = (string) ($_SERVER['HTTP_ORIGIN'] ?? '');
if ($origin !== '' && self::isAllowedOrigin($origin)) {
header('Access-Control-Allow-Origin: ' . $origin);
header('Access-Control-Allow-Credentials: true');
header('Vary: Origin');
}
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Max-Age: 86400');
}
/**
* @param string $origin
*/
private static function isAllowedOrigin($origin)
{
$originHost = parse_url($origin, PHP_URL_HOST);
if (!is_string($originHost) || $originHost === '') {
return false;
}
$requestHost = (string) ($_SERVER['HTTP_HOST'] ?? '');
if ($requestHost !== '' && strcasecmp($originHost, $requestHost) === 0) {
return true;
}
$suffix = self::registrableSuffix($requestHost);
if ($suffix === '') {
return false;
}
$originSuffix = self::registrableSuffix($originHost);
if ($originSuffix === '' || strcasecmp($originSuffix, $suffix) !== 0) {
return false;
}
return self::hostMatchesSuffix($originHost, $suffix);
}
/**
* @param string $host
*/
private static function registrableSuffix($host)
{
$host = strtolower(trim($host));
if ($host === '') {
return '';
}
if (function_exists('cloak_fingerprint_cookie_domain')) {
$cookieDomain = ltrim(cloak_fingerprint_cookie_domain(), '.');
if ($cookieDomain !== '' && self::hostMatchesSuffix($host, $cookieDomain)) {
return $cookieDomain;
}
}
$parts = explode('.', $host);
if (count($parts) < 2) {
return $host;
}
return $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
}
/**
* @param string $host
* @param string $suffix
*/
private static function hostMatchesSuffix($host, $suffix)
{
$host = strtolower($host);
$suffix = strtolower($suffix);
return $host === $suffix || substr($host, -strlen('.' . $suffix)) === '.' . $suffix;
}
}
Regular → Executable
+21 -4
View File
@@ -12,21 +12,38 @@ class CountryAllowlist
*/
public static function parse($config)
{
$config = strtoupper(trim((string) $config));
$config = trim((string) $config);
if ($config === '') {
return [];
}
$parts = preg_split('/\s*,\s*/', $config);
// 支持英文逗号、中文逗号、分号分隔
$parts = preg_split('/\s*[,;]\s*/u', $config);
$out = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '') {
$out[] = $part;
if ($part === '') {
continue;
}
$normalized = CountryCodeNormalizer::toAlpha2($part);
if ($normalized !== null && $normalized !== '') {
$out[] = $normalized;
}
}
return array_values(array_unique($out));
}
/**
* 保存配置前规范化国家列表字符串(dashboard 写入用)
*
* @param string $config
* @return string 如 BR,CN
*/
public static function formatForConfig($config)
{
$list = self::parse($config);
return implode(',', $list);
}
/**
* @param string|null $countryCode
* @param string[] $allowlist
View File
+288
View File
@@ -0,0 +1,288 @@
<?php
/**
* 二次风控(byApiRisk)处理,供 f_check.php 与模拟测试复用
*/
require_once dirname(__DIR__) . '/DbHelper.php';
require_once __DIR__ . '/FpUrlHelper.php';
require_once __DIR__ . '/VisitorApiAudit.php';
require_once __DIR__ . '/RiskSecondaryGate.php';
require_once __DIR__ . '/RiskSecondaryCache.php';
require_once __DIR__ . '/VisitorLogSchema.php';
require_once __DIR__ . '/ClientIpResolver.php';
class FCheckHandler
{
/**
* @param string $hdata base64(JSON)
* @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
*/
public static function processFromHdata($hdata)
{
$decodedString = base64_decode((string) $hdata, true);
$fingerprint_info = is_string($decodedString) ? json_decode($decodedString, true) : null;
if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) {
return [
'ok' => false,
'status' => 400,
'message' => '无效的指纹数据',
];
}
return self::process($fingerprint_info, (string) $hdata);
}
/**
* @param array $fingerprint_info
* @param string|null $hdata base64 payload sent to API;为空时自动生成
* @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
*/
public static function process(array $fingerprint_info, $hdata = null)
{
$fp_host = parse_url($fingerprint_info['domain'], PHP_URL_HOST);
if (empty($fp_host)) {
$fp_host = $_SERVER['HTTP_HOST'] ?? '';
}
if (!class_exists('ConfigLoader', false)) {
require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
}
$config_name = ConfigLoader::loadByHost($fp_host);
$log_id = (int) ($fingerprint_info['log_id'] ?? 0);
if (!RiskSecondaryGate::isEnabled()) {
$_SESSION['visit_to_3'] = 3;
$_SESSION['check_result'] = 'true';
$resolved = FpUrlHelper::resolve($config_name, true);
$fp_url = $resolved['url'];
$response = [
'reason' => '',
'result' => true,
'link' => $fp_url !== '' ? $fp_url : FpUrlHelper::fallbackUrl(),
];
if ($log_id > 0) {
self::updateLog($log_id, [
'result' => 'true',
'reason' => '二次风控已关闭',
'fp_url' => $response['link'],
], $fingerprint_info, null, null);
}
return array_merge(['ok' => true, 'status' => 200, 'fingerprint_info' => $fingerprint_info], $response);
}
$clientIp = ClientIpResolver::resolve();
$cached = RiskSecondaryCache::get($clientIp);
if ($cached !== null) {
return self::finishFromCache($cached, $config_name, $log_id, $fingerprint_info);
}
if ($hdata === null || $hdata === '') {
$json = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$hdata = base64_encode($json !== false ? $json : '{}');
}
$jsonData = [
'id' => COSTM_IP_SCORE,
'hdata' => $hdata,
'referer' => $fingerprint_info['referer'] ?? '',
'domain' => $fingerprint_info['domain'],
'ip' => $fingerprint_info['ip'] ?? '',
'risk_enhanced' => defined('CLOAK_RISK_ENHANCED') && strtoupper(CLOAK_RISK_ENHANCED) === 'ON' ? 1 : 0,
'risk_number' => CLOAK_RISK_NUMBER,
];
$ch = curl_init('www.tiktokba.com/cloak/byApiRisk');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_setopt($ch, CURLOPT_USERAGENT, get_SERVER_value('HTTP_USER_AGENT'));
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-type: application/x-www-form-urlencoded',
'escloak-key: ' . CHECK_KEY,
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($jsonData));
if (function_exists('forward_response_cookies')) {
curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'forward_response_cookies');
}
if (!empty($_COOKIE) && function_exists('encode_visitor_cookies')) {
curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies());
}
$returnRaw = curl_exec($ch);
if ($returnRaw === false) {
$err = curl_error($ch);
curl_close($ch);
return [
'ok' => false,
'status' => 502,
'message' => 'Curl error: ' . $err,
];
}
curl_close($ch);
$return = json_decode($returnRaw, true);
if (!is_array($return)) {
return [
'ok' => false,
'status' => 502,
'message' => '风控 API 返回无效',
];
}
return self::finishFromApi($return, $config_name, $log_id, $fingerprint_info, $jsonData, $clientIp);
}
/**
* @param array $return byApiRisk 原始响应
* @param array $jsonData 请求体
*/
private static function finishFromApi(array $return, $config_name, $log_id, array $fingerprint_info, array $jsonData, $clientIp)
{
$_SESSION['visit_to_3'] = 3;
$response = [];
$response['reason'] = $return['reason'] ?? '';
$response['result'] = !empty($return['result']);
if ($response['result']) {
$_SESSION['check_result'] = 'true';
$resolved = FpUrlHelper::resolve($config_name, true);
$response['link'] = $resolved['url'];
} else {
$_SESSION['check_result'] = 'false';
if (CLOAK_REDIRECT_METHOD == 'curl') {
$response['link'] = '';
} else {
$response['link'] = DB_ZP;
}
}
$update_log = self::buildUpdateLog($response);
self::persistCache($update_log, $clientIp);
if ($log_id > 0) {
self::updateLog($log_id, $update_log, $fingerprint_info, $jsonData, $return);
}
return array_merge([
'ok' => true,
'status' => 200,
'fingerprint_info' => $fingerprint_info,
'api_request' => $jsonData,
'api_response' => $return,
'cached' => false,
], $response);
}
/**
* @param array $cached RiskSecondaryCache::get()
*/
private static function finishFromCache(array $cached, $config_name, $log_id, array $fingerprint_info)
{
RiskSecondaryCache::applyToSession($cached);
$response = RiskSecondaryCache::toApiResponse($cached);
$update_log = [
'result' => $response['result'] ? 'true' : 'false',
'reason' => $response['reason'],
'fp_url' => trim((string) ($response['link'] ?? '')),
];
if ($update_log['fp_url'] === '') {
$update_log['fp_url'] = $update_log['result'] === 'true'
? FpUrlHelper::fallbackUrl()
: (defined('DB_ZP') ? (string) DB_ZP : '');
}
if ($log_id > 0) {
$cacheMarker = [
'cached' => true,
'result' => $update_log['result'],
'reason' => $cached['reason'] ?? '',
'api_reason' => $cached['reason'] ?? '',
'cached_at' => $cached['cached_at'] ?? null,
];
self::updateLog($log_id, $update_log, $fingerprint_info, null, $cacheMarker);
}
return array_merge([
'ok' => true,
'status' => 200,
'fingerprint_info' => $fingerprint_info,
'cached' => true,
], $response);
}
/**
* @param array{result:bool,reason?:string,link?:string} $response
* @return array{result:string,reason:string,fp_url:string}
*/
private static function buildUpdateLog(array $response)
{
$update_log = [];
$update_log['result'] = !empty($response['result']) ? 'true' : 'false';
$update_log['reason'] = $response['reason'] ?? '';
$update_log['fp_url'] = trim((string) ($response['link'] ?? ''));
if ($update_log['fp_url'] === '') {
$update_log['fp_url'] = $update_log['result'] === 'true'
? FpUrlHelper::fallbackUrl()
: (defined('DB_ZP') ? (string) DB_ZP : '');
}
return $update_log;
}
/**
* @param array{result:string,reason:string,fp_url:string} $update_log
* @param string $clientIp
*/
private static function persistCache(array $update_log, $clientIp)
{
RiskSecondaryCache::store([
'ip' => $clientIp,
'result' => $update_log['result'],
'reason' => $update_log['reason'],
'fp_url' => $update_log['fp_url'],
]);
}
/**
* @param int $log_id
* @param array $update_log result, reason, fp_url
* @param array $fingerprint_info
* @param array|null $apiRequest
* @param array|null $apiResponse
*/
public static function updateLog($log_id, array $update_log, array $fingerprint_info, $apiRequest, $apiResponse)
{
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
if (!$conn || $conn->connect_error) {
return false;
}
VisitorLogSchema::ensureColumns($conn);
$resultEsc = cloak_db_escape($conn, $update_log['result'] ?? '');
$reasonEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['reason'] ?? '')));
$fpUrlEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['fp_url'] ?? '')));
$fpHdataJson = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($fpHdataJson === false) {
$fpHdataJson = '';
}
$fpHdataEsc = cloak_db_escape($conn, $fpHdataJson);
$apiRiskReqEsc = cloak_db_escape($conn, $apiRequest !== null ? VisitorApiAudit::encode($apiRequest) : '');
$apiRiskRespEsc = cloak_db_escape($conn, $apiResponse !== null ? VisitorApiAudit::encode($apiResponse) : '');
$sql = "UPDATE `visitor_logs` SET `result`='{$resultEsc}',`reason`='{$reasonEsc}',`fp_url`='{$fpUrlEsc}',`fp_hdata`='{$fpHdataEsc}',`api_risk_request`='{$apiRiskReqEsc}',`api_risk_response`='{$apiRiskRespEsc}' WHERE `id`='" . (int) $log_id . "'";
$ok = $conn->query($sql) === true;
if (!$ok) {
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
if ($handle) {
fwrite($handle, date('Y-m-d H:i:s') . ' | FCheckHandler update: ' . $conn->error . "\n");
fclose($handle);
}
}
$conn->close();
return $ok;
}
}
Regular → Executable
View File
Regular → Executable
View File
View File
Regular → Executable
View File
+39
View File
@@ -3,6 +3,8 @@ require_once __DIR__ . '/../Page/FpPageRenderer.php';
require_once __DIR__ . '/../Page/RiskPageRenderer.php';
require_once __DIR__ . '/../FpUrlHelper.php';
require_once __DIR__ . '/../RiskSecondaryGate.php';
require_once __DIR__ . '/../RiskSecondaryCache.php';
require_once __DIR__ . '/../ClientIpResolver.php';
/**
* 判定完成后的路由输出(真实页 / 安全页 / 二次风控)
@@ -78,6 +80,15 @@ class RouteResolver
if (!isset($_SESSION['visit_to_3'])) {
$_SESSION['visit_to_3'] = '1';
}
if (RiskSecondaryGate::isEnabled()) {
$cached = RiskSecondaryCache::get(ClientIpResolver::resolve());
if ($cached !== null) {
self::renderFromRiskCache($logs, $v_info, $cached);
return;
}
}
if (($_SESSION['visit_to_3'] ?? '1') != '3'
&& RiskSecondaryGate::isEnabled()) {
// 二次风控:先写 wait 状态,f_check 完成后再更新 result / fp_url(避免 true+空跳转)
@@ -103,6 +114,34 @@ class RouteResolver
FpPageRenderer::render(['logs' => $logs]);
}
/**
* 二次风控 Session 缓存命中:跳过 cloakjs / byApiRisk。
*
* @param array $logs
* @param array $cached
*/
private static function renderFromRiskCache(array $logs, $v_info, array $cached)
{
RiskSecondaryCache::applyToSession($cached);
$apiResponse = RiskSecondaryCache::toApiResponse($cached);
$logs['result'] = $apiResponse['result'] ? 'true' : 'false';
$logs['reason'] = $apiResponse['reason'];
$logs['fp_url'] = trim((string) ($apiResponse['link'] ?? ''));
if ($logs['fp_url'] === '') {
$logs['fp_url'] = $logs['result'] === 'true'
? FpUrlHelper::fallbackUrl()
: (defined('DB_ZP') ? (string) DB_ZP : '');
}
if ($logs['result'] === 'false') {
self::renderSafePage($logs, $v_info);
return;
}
FpPageRenderer::render(['logs' => $logs]);
}
/**
* @param array $ctx 需含 v_info, log_id
*/
+4 -1
View File
@@ -35,7 +35,10 @@ if (!CountryAllowlist::isAllowed($countryCode, $allowlist)) {
if ($countryCode === null || $countryCode === '') {
$reason = '无法识别访客国家';
} else {
$reason = '国家不符:' . $countryCode;
$reason = '国家不符:访客 ' . $countryCode;
if ($allowlist !== []) {
$reason .= ',允许 ' . implode(',', $allowlist);
}
}
if (!empty($GLOBALS['__cloak_debug_on'])) {
cloak_dbg_step(__LINE__, 'GeoIP国家判定', 'fail', $reason, [
@@ -1,5 +1,8 @@
<?php
//判断临时链接参数(Guard 之后、API/指纹 之前)
if (!empty($GLOBALS['__cloak_simulation_on'])) {
return;
}
if(empty($_SESSION["check_result"]) && !$whiteParamsMatched && SHOW_SITE_MODE_SWITCH == 'ip_check' && CLOAK_URL_ARGS_TIMEOUT > 0) {
if ($__cloak_debug_on) {
cloak_dbg_step(__LINE__,'临时链接参数', 'info', 'CLOAK_URL_ARGS_TIMEOUT=' . CLOAK_URL_ARGS_TIMEOUT . ' 分钟,开始检查');
@@ -97,6 +97,8 @@
cloak_dbg_step(__LINE__, 'JS 指纹检测', 'skip', 'DEBUG 跳过指纹 JS 跳转,直接进入 API 判定');
CloakDebugPage::setFlag('skipped_fingerprint', true);
$v_info->check_result = 'true';
} elseif (!empty($GLOBALS['__cloak_simulation_on'])) {
$v_info->check_result = 'true';
} else {
FingerprintRedirectRenderer::renderAndExit($config_name);
}
+108
View File
@@ -0,0 +1,108 @@
<?php
require_once __DIR__ . '/ClientIpResolver.php';
require_once __DIR__ . '/FpUrlHelper.php';
/**
* 二次风控(byApiRiskSession 缓存:同 Session 内复用判定结果,IP 变化时失效。
*/
class RiskSecondaryCache
{
const SESSION_KEY = 'cloak_risk_cache';
/**
* @param array{result:string,reason?:string,fp_url?:string,ip?:string} $payload
*/
public static function store(array $payload)
{
$ip = ClientIpResolver::normalizeIp($payload['ip'] ?? ClientIpResolver::resolve());
if ($ip === null) {
return;
}
$_SESSION[self::SESSION_KEY] = [
'ip' => $ip,
'result' => ($payload['result'] ?? '') === 'true' ? 'true' : 'false',
'reason' => (string) ($payload['reason'] ?? ''),
'fp_url' => (string) ($payload['fp_url'] ?? ''),
'risk_number' => defined('CLOAK_RISK_NUMBER') ? (int) CLOAK_RISK_NUMBER : 0,
'campaign_id' => defined('COSTM_IP_SCORE') ? (string) COSTM_IP_SCORE : '',
'cached_at' => time(),
];
}
/**
* @param string|null $currentIp 为空时使用 ClientIpResolver::resolve()
* @return array|null
*/
public static function get($currentIp = null)
{
if (empty($_SESSION[self::SESSION_KEY]) || !is_array($_SESSION[self::SESSION_KEY])) {
return null;
}
$cached = $_SESSION[self::SESSION_KEY];
$ip = ClientIpResolver::normalizeIp($currentIp !== null && $currentIp !== '' ? $currentIp : ClientIpResolver::resolve());
$cachedIp = ClientIpResolver::normalizeIp($cached['ip'] ?? '');
$invalidate = false;
if ($ip === null || $cachedIp === null || $ip !== $cachedIp) {
$invalidate = true;
} elseif (defined('CLOAK_RISK_NUMBER') && (int) ($cached['risk_number'] ?? -1) !== (int) CLOAK_RISK_NUMBER) {
$invalidate = true;
} elseif (defined('COSTM_IP_SCORE') && (string) ($cached['campaign_id'] ?? '') !== (string) COSTM_IP_SCORE) {
$invalidate = true;
}
if ($invalidate) {
self::clear();
$_SESSION['visit_to_3'] = '1';
return null;
}
return $cached;
}
public static function clear()
{
unset($_SESSION[self::SESSION_KEY]);
}
/**
* 将缓存同步到 visit_to_3 / check_result,供 RouteResolver 等复用。
*
* @param array $cached
*/
public static function applyToSession(array $cached)
{
$_SESSION['visit_to_3'] = 3;
$_SESSION['check_result'] = ($cached['result'] ?? '') === 'true' ? 'true' : 'false';
}
public static function hitReason()
{
return '二次风控缓存命中';
}
/**
* @param array $cached
* @return array{result:bool,reason:string,link:string}
*/
public static function toApiResponse(array $cached)
{
$passed = ($cached['result'] ?? '') === 'true';
$link = (string) ($cached['fp_url'] ?? '');
if ($passed && $link === '') {
$link = FpUrlHelper::fallbackUrl();
}
if (!$passed && $link === '' && defined('DB_ZP')) {
$link = (string) DB_ZP;
}
return [
'result' => $passed,
'reason' => self::hitReason(),
'link' => $link,
];
}
}
Regular → Executable
View File
+149
View File
@@ -0,0 +1,149 @@
<?php
require_once __DIR__ . '/FpUrlHelper.php';
require_once __DIR__ . '/RiskSecondaryGate.php';
require_once __DIR__ . '/RiskSecondaryCache.php';
require_once __DIR__ . '/ClientIpResolver.php';
/**
* 模拟测试路由:写日志,不输出 HTML / 302
*/
class SimulationRouteResolver
{
/**
* @param array $ctx 需含 v_info, logs, config_name, reason, model
* @return array{branch:string,log_id:?int,logs:array,needs_secondary:bool}
*/
public static function dispatch(array $ctx)
{
extract($ctx, EXTR_SKIP);
if (SHOW_SITE_MODE_SWITCH == 'zp') {
return self::safeBranch($logs, $v_info);
}
$showFp = (SHOW_SITE_MODE_SWITCH == 'ip_check' && $_SESSION['check_result'] != 'false')
|| SHOW_SITE_MODE_SWITCH == 'fp';
if ($showFp) {
return self::fpOrRiskBranch($logs, $v_info, $config_name);
}
return self::safeBranch($logs, $v_info);
}
/**
* @param array $logs
*/
private static function safeBranch(array $logs, $v_info)
{
if (SHOW_SITE_MODE_SWITCH == 'zp') {
$logs['result'] = 'false';
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', '正品模式');
}
$logs['fp_url'] = DB_ZP;
$log_id = write_log_db($logs);
return [
'branch' => 'safe_page',
'log_id' => $log_id,
'logs' => $logs,
'needs_secondary' => false,
];
}
/**
* @param array $logs
*/
private static function fpOrRiskBranch(array $logs, $v_info, $config_name)
{
if (AUTO_BLACK == 'ON') {
$times = howmanytimesbyip($v_info->v_ip);
if ($times + 1 >= AUTO_BLACK_TIMES) {
write_black_list($v_info->v_ip);
}
}
if (!isset($_SESSION['visit_to_3'])) {
$_SESSION['visit_to_3'] = '1';
}
if (RiskSecondaryGate::isEnabled()) {
$cached = RiskSecondaryCache::get(ClientIpResolver::resolve());
if ($cached !== null) {
return self::fpFromRiskCache($logs, $v_info, $config_name, $cached);
}
}
if (($_SESSION['visit_to_3'] ?? '1') != '3' && RiskSecondaryGate::isEnabled()) {
$logs['result'] = 'wait';
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', '二次风控检测中');
$logs['fp_url'] = '';
$log_id = write_log_db($logs);
return [
'branch' => 'secondary_risk',
'log_id' => $log_id,
'logs' => $logs,
'needs_secondary' => true,
];
}
$logs['result'] = 'true';
$logs['reason'] = $logs['reason'] ?? '';
$resolved = FpUrlHelper::resolve($config_name, true);
$logs['fp_url'] = $resolved['url'] !== '' ? $resolved['url'] : FpUrlHelper::fallbackUrl();
$log_id = write_log_db($logs);
return [
'branch' => 'fp_page',
'log_id' => $log_id,
'logs' => $logs,
'needs_secondary' => false,
];
}
/**
* @param array $logs
* @param array $cached
*/
private static function fpFromRiskCache(array $logs, $v_info, $config_name, array $cached)
{
RiskSecondaryCache::applyToSession($cached);
$apiResponse = RiskSecondaryCache::toApiResponse($cached);
$logs['result'] = $apiResponse['result'] ? 'true' : 'false';
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', $apiResponse['reason']);
$logs['fp_url'] = trim((string) ($apiResponse['link'] ?? ''));
if ($logs['fp_url'] === '') {
$logs['fp_url'] = $logs['result'] === 'true'
? FpUrlHelper::fallbackUrl()
: (defined('DB_ZP') ? (string) DB_ZP : '');
}
if ($logs['result'] === 'false') {
return self::safeBranch($logs, $v_info);
}
$log_id = write_log_db($logs);
return [
'branch' => 'fp_page_cache',
'log_id' => $log_id,
'logs' => $logs,
'needs_secondary' => false,
];
}
private static function mergeReason($existing, $append)
{
$existing = trim((string) $existing);
$append = trim((string) $append);
if ($existing === '') {
return $append;
}
if ($append === '' || strpos($existing, $append) !== false) {
return $existing;
}
return $existing . '' . $append;
}
}
Regular → Executable
View File
View File
+135
View File
@@ -0,0 +1,135 @@
<?php
/**
* 访客日志列表查询与行格式化(datalist / sdatalist 共用,避免重复重逻辑)
*/
class VisitorLogListHelper
{
/**
* @param mysqli $conn
* @param array $get
*/
public static function buildWhere($conn, array $get)
{
$where = '1';
$where .= cloak_visitor_log_apply_result_filter($conn, $get['result'] ?? '');
$map = [];
if (!empty($get['visit_date'])) {
$day = trim((string) $get['visit_date']);
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $day)) {
$dayEsc = cloak_db_escape($conn, $day);
$map[] = "`visit_date` >= '{$dayEsc} 00:00:00' AND `visit_date` < DATE_ADD('{$dayEsc} 00:00:00', INTERVAL 1 DAY)";
}
}
if (!empty($get['country'])) {
$map[] = "country LIKE '" . cloak_db_escape($conn, $get['country']) . "'";
}
if (!empty($get['IP'])) {
$map[] = "IP LIKE '" . cloak_db_escape($conn, $get['IP']) . "'";
}
if (!empty($get['domain'])) {
$map[] = "domain LIKE '%" . cloak_db_escape($conn, $get['domain']) . "%'";
}
if (!empty($get['spage'])) {
$map[] = "page LIKE '%" . cloak_db_escape($conn, $get['spage']) . "%'";
}
if ($map !== []) {
$where .= ' AND ' . implode(' AND ', $map);
}
return $where;
}
/**
* 列表轻量 SELECT(不拉取 fp_hdata / api_* 等大 TEXT
*
* @param bool $withSimulation
*/
public static function listSelectSql($where, $startPosition, $recordsPerPage, $withSimulation = true)
{
$cols = '`id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `device`, `judge_timing`, '
. '(CASE WHEN `fp_hdata` IS NOT NULL AND `fp_hdata` <> \'\' THEN 1 ELSE 0 END) AS `has_fp_hdata`, '
. '(CASE WHEN `api_by_request` IS NOT NULL AND `api_by_request` <> \'\' THEN 1 ELSE 0 END) AS `has_api_by_request`, '
. '(CASE WHEN `api_by_response` IS NOT NULL AND `api_by_response` <> \'\' THEN 1 ELSE 0 END) AS `has_api_by_response`, '
. '(CASE WHEN `api_risk_request` IS NOT NULL AND `api_risk_request` <> \'\' THEN 1 ELSE 0 END) AS `has_api_risk_request`, '
. '(CASE WHEN `api_risk_response` IS NOT NULL AND `api_risk_response` <> \'\' THEN 1 ELSE 0 END) AS `has_api_risk_response`, '
. '`add_time`';
if ($withSimulation) {
$cols .= ', `is_simulation`, `sim_source_log_id`';
}
$startPosition = (int) $startPosition;
$recordsPerPage = (int) $recordsPerPage;
return "SELECT {$cols} FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}";
}
/**
* @param array $row
* @param array $countries
* @param array $reasons
*/
public static function formatListRow(array $row, array $countries, array $reasons)
{
if (!empty($row['country'])) {
$row['country'] = $countries[$row['country']] ?? '无';
} else {
$row['country'] = '无';
}
if (isset($reasons[$row['reason']])) {
$row['reason'] = $reasons[$row['reason']];
}
if (($row['result'] ?? '') === 'wait') {
$row['result'] = '检测中';
}
$row['judge_timing_text'] = cloak_visitor_log_format_timing($row['judge_timing'] ?? '');
if (!empty($row['has_fp_hdata'])) {
$row['fp_hdata_text'] = '有指纹数据';
} else {
$row['fp_hdata_text'] = '';
}
$row['api_by_request_text'] = self::auditPlaceholder($row, 'has_api_by_request');
$row['api_by_response_text'] = self::auditPlaceholder($row, 'has_api_by_response');
$row['api_risk_request_text'] = self::auditPlaceholder($row, 'has_api_risk_request');
$row['api_risk_response_text'] = self::auditPlaceholder($row, 'has_api_risk_response');
unset(
$row['has_fp_hdata'],
$row['has_api_by_request'],
$row['has_api_by_response'],
$row['has_api_risk_request'],
$row['has_api_risk_response']
);
return $row;
}
/**
* @param array $row
* @param string $flagKey
*/
private static function auditPlaceholder(array $row, $flagKey)
{
return !empty($row[$flagKey]) ? '__LAZY__' : '-';
}
/**
* @param mysqli $conn
* @param string $where
*/
public static function countWhere($conn, $where)
{
$sql = "SELECT COUNT(*) AS `total` FROM `visitor_logs` WHERE {$where}";
$res = $conn->query($sql);
if (!$res) {
return 0;
}
$row = $res->fetch_assoc();
$res->free();
return (int) ($row['total'] ?? 0);
}
}
Regular → Executable
View File
+3
View File
@@ -28,6 +28,8 @@ class VisitorLogSchema
'api_by_response' => "ALTER TABLE `visitor_logs` ADD COLUMN `api_by_response` TEXT NULL COMMENT 'byApi 响应 JSON' AFTER `api_by_request`",
'api_risk_request' => "ALTER TABLE `visitor_logs` ADD COLUMN `api_risk_request` TEXT NULL COMMENT 'byApiRisk 请求参数 JSON' AFTER `api_by_response`",
'api_risk_response' => "ALTER TABLE `visitor_logs` ADD COLUMN `api_risk_response` TEXT NULL COMMENT 'byApiRisk 响应 JSON' AFTER `api_risk_request`",
'is_simulation' => "ALTER TABLE `visitor_logs` ADD COLUMN `is_simulation` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否模拟测试' AFTER `api_risk_response`",
'sim_source_log_id' => "ALTER TABLE `visitor_logs` ADD COLUMN `sim_source_log_id` INT UNSIGNED NULL COMMENT '模拟重放来源日志 ID' AFTER `is_simulation`",
];
foreach ($columns as $name => $ddl) {
if (!self::columnExists($conn, 'visitor_logs', $name)) {
@@ -50,6 +52,7 @@ class VisitorLogSchema
'idx_result' => 'ALTER TABLE `visitor_logs` ADD INDEX `idx_result` (`result`)',
'idx_domain_date' => 'ALTER TABLE `visitor_logs` ADD INDEX `idx_domain_date` (`domain`, `visit_date`)',
'idx_country' => 'ALTER TABLE `visitor_logs` ADD INDEX `idx_country` (`country`)',
'idx_ip_domain' => 'ALTER TABLE `visitor_logs` ADD INDEX `idx_ip_domain` (`IP`, `domain`)',
];
foreach ($indexes as $name => $ddl) {
if (!self::indexExists($conn, 'visitor_logs', $name)) {
+41
View File
@@ -0,0 +1,41 @@
<?php
/**
* 访客日志「第几次访问」:写入时计算,避免列表接口 N+1 查询。
*/
class VisitorLogVtimes
{
/**
* @param mysqli $conn
* @param string $ip
* @param string $domain
*/
public static function resolveForInsert($conn, $ip, $domain)
{
$ipEsc = cloak_db_escape($conn, $ip);
$domainEsc = cloak_db_escape($conn, $domain);
$sql = "SELECT COUNT(*) AS `c` FROM `visitor_logs` WHERE `IP`='{$ipEsc}' AND `domain`='{$domainEsc}'";
$count = 0;
if ($res = $conn->query($sql)) {
$row = $res->fetch_assoc();
$count = (int) ($row['c'] ?? 0);
$res->free();
}
return $count + 1;
}
/**
* @param array $logs cloak_write_log_db 结构
*/
public static function mergeIntoLogs(array $logs, $conn)
{
if (!empty($logs['vtimes']) && (int) $logs['vtimes'] > 0) {
return $logs;
}
$ip = (string) ($logs['customers_ip'] ?? '');
$domain = (string) ($logs['site_name'] ?? '');
if ($ip !== '' && $domain !== '') {
$logs['vtimes'] = self::resolveForInsert($conn, $ip, $domain);
}
return $logs;
}
}
Regular → Executable
+32 -4
View File
@@ -5,6 +5,7 @@
require_once dirname(__DIR__) . '/DbHelper.php';
require_once __DIR__ . '/VisitorLogSchema.php';
require_once __DIR__ . '/VisitorLogQueue.php';
require_once __DIR__ . '/VisitorLogVtimes.php';
class VisitorLogWorker
{
@@ -65,12 +66,14 @@ class VisitorLogWorker
VisitorLogSchema::ensureColumns($conn);
VisitorLogSchema::ensureIndexes($conn);
$logs = VisitorLogVtimes::mergeIntoLogs($logs, $conn);
$data = self::escapeLogs($conn, $logs);
$d = static function ($key) use ($data, $conn) {
return array_key_exists($key, $data) ? $data[$key] : cloak_db_escape($conn, '');
};
list($simCols, $simVals) = self::simulationInsertFragment($data);
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`) VALUES ('"
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`{$simCols}) VALUES ('"
. $d('campagin_id') . "','"
. $d('visit_md5_code') . "','"
. $d('site_name') . "','"
@@ -80,6 +83,7 @@ class VisitorLogWorker
. $d('result') . "','"
. $d('reason') . "','"
. $d('v_referer') . "','"
. (int) ($data['vtimes'] ?? 1) . "','"
. $d('Client') . "','"
. $d('v_Browser') . "','"
. $d('v_PageURL') . "','"
@@ -93,7 +97,8 @@ class VisitorLogWorker
. $d('api_by_request') . "','"
. $d('api_by_response') . "','"
. $d('api_risk_request') . "','"
. $d('api_risk_response') . "')";
. $d('api_risk_response') . "'"
. $simVals . ')';
if ($conn->query($sql) !== true) {
VisitorLogQueue::logError('insert_full: ' . $conn->error . ' | ' . $sql);
@@ -120,12 +125,14 @@ class VisitorLogWorker
VisitorLogSchema::ensureColumns($conn);
VisitorLogSchema::ensureIndexes($conn);
$logs = VisitorLogVtimes::mergeIntoLogs($logs, $conn);
$data = self::escapeLogs($conn, $logs);
$d = static function ($key) use ($data, $conn) {
return array_key_exists($key, $data) ? $data[$key] : cloak_db_escape($conn, '');
};
list($simCols, $simVals) = self::simulationInsertFragment($data);
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `client`, `browser`, `language`, `fp_url`, `device`) VALUES ('"
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `vtimes`, `client`, `browser`, `language`, `fp_url`, `device`{$simCols}) VALUES ('"
. $d('campagin_id') . "','"
. $d('visit_md5_code') . "','"
. $d('site_name') . "','"
@@ -134,11 +141,13 @@ class VisitorLogWorker
. $d('country') . "','"
. $d('result') . "','"
. $d('reason') . "','"
. (int) ($data['vtimes'] ?? 1) . "','"
. $d('Client') . "','"
. $d('v_Browser') . "','"
. $d('accept_language') . "','"
. $d('fp_url') . "','"
. $d('device') . "')";
. $d('device') . "'"
. $simVals . ')';
if ($conn->query($sql) !== true) {
VisitorLogQueue::logError('insertMinimal: ' . $conn->error);
@@ -216,6 +225,25 @@ class VisitorLogWorker
return $data;
}
/**
* @param array<string, string> $data escaped log fields
* @return array{0:string,1:string}
*/
private static function simulationInsertFragment(array $data)
{
if (empty($data['is_simulation']) && !array_key_exists('sim_source_log_id', $data)) {
return ['', ''];
}
$cols = ', `is_simulation`, `sim_source_log_id`';
$isSim = (int) ($data['is_simulation'] ?? 0);
if (!empty($data['sim_source_log_id'])) {
$vals = ", '{$isSim}', '" . (int) $data['sim_source_log_id'] . "'";
} else {
$vals = ", '{$isSim}', NULL";
}
return [$cols, $vals];
}
/**
* @return mysqli|null
*/
+32 -2
View File
@@ -5,6 +5,7 @@
require_once dirname(__DIR__) . '/DbHelper.php';
require_once dirname(__DIR__) . '/CloakHttpHelpers.php';
require_once __DIR__ . '/VisitorApiAudit.php';
require_once __DIR__ . '/VisitorLogVtimes.php';
if (!function_exists('cloak_session_mark_real_visitor')) {
/**
@@ -44,6 +45,9 @@ if (!function_exists('cloak_session_clear_real_visitor')) {
$_SESSION['gcu_Browser'],
$_SESSION['gcu_referer']
);
if (class_exists('RiskSecondaryCache', false)) {
RiskSecondaryCache::clear();
}
setCrossDomainCookie('gfuid', '', time() - 3600);
}
}
@@ -164,6 +168,17 @@ if (!function_exists('cloak_write_log_db')) {
{
$logs = cloak_visitor_log_enrich($logs);
if (!empty($GLOBALS['__cloak_force_sync_log'])) {
if (($logs['result'] ?? '') === 'wait') {
$logId = VisitorLogWorker::insertMinimal($logs);
if ($logId !== null) {
VisitorLogWorker::enrich($logId, $logs);
}
return $logId;
}
return cloak_write_log_db_sync($logs);
}
if (defined('WRITE_LOG') && WRITE_LOG == '1') {
$my_url = $_SERVER['PHP_SELF'] ?? '';
$my_file_name = substr($my_url, strrpos($my_url, '/') + 1);
@@ -203,6 +218,8 @@ if (!function_exists('cloak_write_log_db')) {
return null;
}
VisitorLogSchema::ensureColumns($conn);
VisitorLogSchema::ensureIndexes($conn);
$logs = VisitorLogVtimes::mergeIntoLogs($logs, $conn);
$data = [];
foreach ($logs as $key => $value) {
$data[$key] = cloak_db_escape($conn, $value);
@@ -210,7 +227,18 @@ if (!function_exists('cloak_write_log_db')) {
$d = static function ($key) use ($data, $conn) {
return array_key_exists($key, $data) ? $data[$key] : cloak_db_escape($conn, '');
};
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`) VALUES ('"
$simCols = '';
$simVals = '';
if (!empty($data['is_simulation']) || array_key_exists('sim_source_log_id', $data)) {
$simCols = ', `is_simulation`, `sim_source_log_id`';
$isSim = (int) ($data['is_simulation'] ?? 0);
if (!empty($data['sim_source_log_id'])) {
$simVals = ", '{$isSim}', '" . (int) $data['sim_source_log_id'] . "'";
} else {
$simVals = ", '{$isSim}', NULL";
}
}
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`{$simCols}) VALUES ('"
. $d('campagin_id') . "','"
. $d('visit_md5_code') . "','"
. $d('site_name') . "','"
@@ -220,6 +248,7 @@ if (!function_exists('cloak_write_log_db')) {
. $d('result') . "','"
. $d('reason') . "','"
. $d('v_referer') . "','"
. (int) ($data['vtimes'] ?? 1) . "','"
. $d('Client') . "','"
. $d('v_Browser') . "','"
. $d('v_PageURL') . "','"
@@ -233,7 +262,8 @@ if (!function_exists('cloak_write_log_db')) {
. $d('api_by_request') . "','"
. $d('api_by_response') . "','"
. $d('api_risk_request') . "','"
. $d('api_risk_response') . "')";
. $d('api_risk_response') . "'"
. $simVals . ')';
if ($conn->query($sql) === true) {
$last_id = $conn->insert_id;
$conn->close();
+591
View File
@@ -0,0 +1,591 @@
<?php
/**
* 模拟测试访问:重建请求环境并跑完整判定链(含二次风控)
*/
require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
require_once __DIR__ . '/SimulationRouteResolver.php';
require_once __DIR__ . '/FCheckHandler.php';
require_once __DIR__ . '/DomainRepository.php';
class VisitorSimulationRunner
{
const REASON_PREFIX = '[模拟测试]';
/**
* @param array $input
* @return array
*/
public static function runManual(array $input)
{
$context = self::normalizeManualInput($input);
if (!empty($context['error'])) {
return ['ok' => 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;
}
}
}
}
Regular → Executable
View File