diff --git a/admin/ConfigWriter.php b/admin/ConfigWriter.php index ef74ac5..bfb3d39 100755 --- a/admin/ConfigWriter.php +++ b/admin/ConfigWriter.php @@ -2,6 +2,8 @@ /** * 生成 check_config/*.php(define 拼接,输出格式与历史 dashboard 一致) */ +require_once dirname(__DIR__) . '/lib/Cloak/CountryAllowlist.php'; + class ConfigWriter { /** @@ -14,7 +16,7 @@ class ConfigWriter $methods = $data['methods']; $blacklist_group_id = (int) $data['blacklist_group_id']; $whitelist_group_id = (int) $data['whitelist_group_id']; - $country = $data['country']; + $country = CountryAllowlist::formatForConfig($data['country']); $show_content = $data['show_content']; $DB_fp = $data['DB_fp']; $costm_ip_score = $data['costm_ip_score']; diff --git a/check_config/indBXSXX_config.php b/check_config/indBXSXX_config.php new file mode 100644 index 0000000..a9dde26 --- /dev/null +++ b/check_config/indBXSXX_config.php @@ -0,0 +1,40 @@ + 'https://www.google.com/', +) ); + define('COSTM_IP_SCORE', '20251108_emoUAVF6S' ); + define('BLACK_LIST', '0' ); + define('AUTO_BLACK', 'OFF' ); + define('IS_VIRTUAL', '2' ); + define('AUTO_BLACK_TIMES', '5' ); + define('WHITE_PARAMS', '' ); + define('IPHONE_MODEL', 0 ); + + define('SHOW_SITE_MOBILE', 0 ); + define('SHOW_SITE_OUTPUT', 1 ); + define('WRITE_LOG', 1 ); + define('CLOAK_ZH_ON', 'OFF'); + define('CLOAK_OS_ON', 'OFF'); + define('KEEP_PARAMS', 'OFF'); + define('CLOAK_REDIRECT_METHOD', 'curl'); + define('DB_ZP', 'https://worldhomenest.store'); + define('CLOAK_MOBILE_ON', 'ON'); + define('CLOAK_V_REFERER', 'OFF'); + define('CLOAK_RISK_NUMBER', '68'); + define('CLOAK_RISK_ENHANCED', 'ON'); + define('CLOAK_URL_ARGS_TIMEOUT', '0'); + + define('CLOAK_AD_FB', 'OFF'); + define('CLOAK_AD_GOOGLE', 'ON'); + define('CLOAK_AD_TIKTOK', 'ON'); + define('CLOAK_AD_STRICT', 'OFF'); + define('CLOAK_AD_SPM_ID', 'Ss4yqlAnuG1V'); + + define('CLOAK_DEBUG_MODE', 'OFF'); + ?> \ No newline at end of file diff --git a/cloakjs.php b/cloakjs.php index aa037da..d045d79 100755 --- a/cloakjs.php +++ b/cloakjs.php @@ -58,7 +58,7 @@ if ($__cloak_risk_enhanced) { /* ===== 指纹采集 + 提交(轻量字段始终采集;耗时采集仅加强判断 ON) ===== */ (async function () { - const __cloakRiskEnhanced = ; + const __cloakRiskEnhanced = ; try { let _0x3b0528; let _0x12fad4; diff --git a/composer.json b/composer.json old mode 100644 new mode 100755 diff --git a/composer.lock b/composer.lock old mode 100644 new mode 100755 diff --git a/config/ConfigLoader.php b/config/ConfigLoader.php index 8ef3200..7416519 100755 --- a/config/ConfigLoader.php +++ b/config/ConfigLoader.php @@ -41,13 +41,22 @@ class ConfigLoader } /** - * 优先 HTTP Host(多站/宝塔),否则入口脚本名(单站兼容) + * 同域多落地页:入口脚本有独立配置时优先脚本名;否则按 HTTP Host(多站/宝塔);再回退脚本名或 index * * @return string 实际加载的配置名 */ public static function loadForRequest($scriptBaseName, $host = null) { $host = $host ?? ($_SERVER['HTTP_HOST'] ?? ''); + $baseDir = dirname(__DIR__); + $scriptSanitized = self::sanitizeConfigName((string) $scriptBaseName); + $scriptConfigFile = $baseDir . '/check_config/' . $scriptSanitized . '_config.php'; + $scriptConfigExists = $scriptSanitized !== 'index' + && $scriptSanitized !== 'ip_check' + && file_exists($scriptConfigFile); + if ($scriptConfigExists) { + return self::loadByName($scriptSanitized); + } if ($host !== '') { return self::loadByHost($host); } diff --git a/dashboard.php b/dashboard.php index 6a8e8d0..9317a1f 100755 --- a/dashboard.php +++ b/dashboard.php @@ -890,7 +890,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def 指定国家访问真实页(逗号分隔国家代码) - + 广告策略 ID diff --git a/datatable/auth.php b/datatable/auth.php new file mode 100644 index 0000000..8279ef2 --- /dev/null +++ b/datatable/auth.php @@ -0,0 +1,18 @@ + "过滤无来源的访问", @@ -45,20 +47,9 @@ $reasons = array( "time_of_day" => "不可访问的日期", ); -if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) { - unset($_SESSION['password']); - unset($_SESSION['check_result']); - die("404 Not Found"); -} -$servername = "localhost"; -$username = DB_USERNAME; -$password = DB_PASSWORD; -$dbname = DB_NAME; - -// Create connection -$conn = new mysqli($servername, $username, $password, $dbname); $jsonResult = array(); -// Check connection +$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); + if ($conn->connect_error) { $error = "Connection failed: " . $conn->connect_error; $handle = fopen(dirname(__FILE__) . "/err.txt", "a+"); @@ -71,123 +62,40 @@ if ($conn->connect_error) { $jsonResult["data"] = array(); } else { VisitorLogSchema::ensureColumns($conn); - if(isset($_GET['clear']) && $_GET['clear'] == "true") { //清除日志数据表 - $sql = "TRUNCATE `visitor_logs`"; - - // 执行查询并输出结果 - $result = mysqli_query($conn, $sql); + VisitorLogSchema::ensureIndexes($conn); + + if(isset($_GET['clear']) && $_GET['clear'] == "true") { + $conn->query("TRUNCATE `visitor_logs`"); $jsonResult["code"] = 0; $jsonResult["msg"] = "success"; $jsonResult["count"] = 0; $jsonResult["data"] = array(); - } else { // 默认分页数据 - // 获取当前页码(默认为第1页) - $pageNumber = isset($_GET['page']) ? (int) $_GET['page'] : 1; - - // 设置每页显示的记录条数 - $recordsPerPage = isset($_GET['limit']) ? (int) $_GET['limit'] : 50; - - // 计算起始位置 + } else { + $pageNumber = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; + $recordsPerPage = isset($_GET['limit']) ? max(1, min(200, (int) $_GET['limit'])) : 50; $startPosition = ($pageNumber - 1) * $recordsPerPage; - $map = array(); - $where = 1; - - if(!empty($_GET['visit_date'])) { - $map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'"; + $where = VisitorLogListHelper::buildWhere($conn, $_GET); + + $sql = VisitorLogListHelper::listSelectSql($where, $startPosition, $recordsPerPage, true); + $result = $conn->query($sql); + $jsonData = array(); + + if ($result) { + while ($row = $result->fetch_assoc()) { + $jsonData[] = VisitorLogListHelper::formatListRow($row, $countries, $reasons); + } + $result->free(); } - $where .= cloak_visitor_log_apply_result_filter($conn, $_GET['result'] ?? ''); - - 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(!empty($map)) { - $where .= " AND " . join(" AND ", $map); - } - // SQL语句 - $sql = "SELECT `id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_hdata`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`, `device`, `add_time` FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}"; - - // 执行查询并输出结果 - $result = mysqli_query($conn, $sql); - $jsonData = array(); - - while ($row = mysqli_fetch_assoc($result)) { - //echo "

{$row["columnName"]}

"; // 根据需要修改列名 - if($row['vtimes'] == 0) { - $row['vtimes'] = 1; // 还未统计访问次数 - $sqlVtimes = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE `IP` = '" . cloak_db_escape($conn, $row["IP"]) . "' AND `domain` = '" . cloak_db_escape($conn, $row["domain"]) . "' AND id < " . (int)$row['id']); - $vtimesRow = mysqli_fetch_assoc($sqlVtimes); - $vtimes = $vtimesRow["total"]; // 如果之前此IP有访问记录 - if($vtimes > 0) { - $row['vtimes'] += $vtimes; // 访问次数叠加 - } - - // 更新访问次数 - $updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id']; - mysqli_query($conn, $updateSql); - } - - if(!empty($row['country'])) { - $row['country'] = isset($countries[$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'] ?? ''); - $row['fp_hdata_text'] = cloak_visitor_log_format_fp_hdata($row['fp_hdata'] ?? ''); - if (!empty($row['fp_hdata'])) { - $explain = cloak_visitor_log_fp_risk_explain( - $row['fp_hdata'], - $row['reason'] ?? '', - $row['result'] ?? '', - $row['domain'] ?? '' - ); - $row['fp_hdata_explain_summary'] = $explain['summary']; - $row['fp_hdata_explain_html'] = $explain['html']; - $row['fp_hdata_explain'] = $explain['data']; - } else { - $row['fp_hdata_explain_summary'] = ''; - $row['fp_hdata_explain_html'] = ''; - $row['fp_hdata_explain'] = null; - } - $row['api_by_request_text'] = VisitorApiAudit::displayText($row['api_by_request'] ?? ''); - $row['api_by_response_text'] = VisitorApiAudit::displayText($row['api_by_response'] ?? ''); - $row['api_risk_request_text'] = VisitorApiAudit::displayText($row['api_risk_request'] ?? ''); - $row['api_risk_response_text']= VisitorApiAudit::displayText($row['api_risk_response'] ?? ''); - $jsonData[] = $row; - } - - $sqlCount = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE {$where}"); - $countRow = mysqli_fetch_assoc($sqlCount); - $jsonResult["code"] = 0; $jsonResult["msg"] = ""; - $jsonResult["count"] = $countRow['total']; + $jsonResult["count"] = VisitorLogListHelper::countWhere($conn, $where); $jsonResult["data"] = $jsonData; } } - -// 关闭数据库连接 -mysqli_close($conn); -header('Content-Type: application/json'); +$conn->close(); -echo json_encode($jsonResult);exit; +header('Content-Type: application/json; charset=utf-8'); +echo json_encode($jsonResult); +exit; diff --git a/datatable/fp_explain.php b/datatable/fp_explain.php new file mode 100644 index 0000000..9ce91ec --- /dev/null +++ b/datatable/fp_explain.php @@ -0,0 +1,55 @@ + 1, 'msg' => 'invalid id']); + exit; +} + +$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); +if ($conn->connect_error) { + echo json_encode(['code' => 1, 'msg' => 'db error']); + exit; +} + +$sql = "SELECT `id`, `fp_hdata`, `reason`, `result`, `domain` FROM `visitor_logs` WHERE `id`=" . $id . ' LIMIT 1'; +$res = $conn->query($sql); +$row = $res ? $res->fetch_assoc() : null; +if ($res) { + $res->free(); +} +$conn->close(); + +if (!$row || empty($row['fp_hdata'])) { + echo json_encode(['code' => 1, 'msg' => 'no fp_hdata']); + exit; +} + +$result = $row['result'] ?? ''; +if ($result === 'wait') { + $result = '检测中'; +} + +$explain = cloak_visitor_log_fp_risk_explain( + $row['fp_hdata'], + $row['reason'] ?? '', + $result, + $row['domain'] ?? '' +); + +echo json_encode([ + 'code' => 0, + 'msg' => '', + 'summary' => $explain['summary'], + 'html' => $explain['html'], +], JSON_UNESCAPED_UNICODE); diff --git a/datatable/log_analytics.php b/datatable/log_analytics.php old mode 100644 new mode 100755 diff --git a/datatable/log_blob.php b/datatable/log_blob.php new file mode 100644 index 0000000..b5343a7 --- /dev/null +++ b/datatable/log_blob.php @@ -0,0 +1,58 @@ + 'fp_hdata', + 'api_by_request' => 'api_by_request', + 'api_by_response' => 'api_by_response', + 'api_risk_request' => 'api_risk_request', + 'api_risk_response' => 'api_risk_response', + 'user_agent' => 'user_agent', + 'http_referer' => 'http_referer', + 'accept_language_raw' => 'accept_language_raw', +]; + +if ($id <= 0 || !isset($allowed[$field])) { + echo json_encode(['code' => 1, 'msg' => 'invalid request']); + exit; +} + +$col = $allowed[$field]; +$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); +if ($conn->connect_error) { + echo json_encode(['code' => 1, 'msg' => 'db error']); + exit; +} + +$sql = 'SELECT `' . $col . '` AS `blob` FROM `visitor_logs` WHERE `id`=' . $id . ' LIMIT 1'; +$res = $conn->query($sql); +$row = $res ? $res->fetch_assoc() : null; +if ($res) { + $res->free(); +} +$conn->close(); + +$blob = trim((string) ($row['blob'] ?? '')); +if ($blob === '') { + echo json_encode(['code' => 1, 'msg' => 'empty']); + exit; +} + +echo json_encode([ + 'code' => 0, + 'msg' => '', + 'field' => $field, + 'data' => $blob, +], JSON_UNESCAPED_UNICODE); diff --git a/datatable/sdatalist.php b/datatable/sdatalist.php index 249f2d4..001b1d0 100755 --- a/datatable/sdatalist.php +++ b/datatable/sdatalist.php @@ -4,7 +4,7 @@ require_once(dirname(__DIR__) . "/cong.php"); require_once(dirname(__DIR__) . "/lib/DbHelper.php"); require_once(dirname(__DIR__) . "/lib/Cloak/PipelineTimer.php"); require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogSchema.php"); -require_once(dirname(__DIR__) . "/lib/Cloak/VisitorApiAudit.php"); +require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogListHelper.php"); $reasons = array( "blank_referrer" => "过滤无来源的访问", @@ -44,15 +44,9 @@ $reasons = array( "time_of_day" => "不可访问的日期", ); -$servername = "localhost"; -$username = DB_USERNAME; -$password = DB_PASSWORD; -$dbname = DB_NAME; - -// Create connection -$conn = new mysqli($servername, $username, $password, $dbname); $jsonResult = array(); -// Check connection +$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); + if ($conn->connect_error) { $error = "Connection failed: " . $conn->connect_error; $handle = fopen(dirname(__FILE__) . "/err.txt", "a+"); @@ -65,119 +59,40 @@ if ($conn->connect_error) { $jsonResult["data"] = array(); } else { VisitorLogSchema::ensureColumns($conn); - if(isset($_GET['clear']) && $_GET['clear'] == "true") { //清除日志数据表 - $sql = "TRUNCATE `visitor_logs`"; - - // 执行查询并输出结果 - $result = mysqli_query($conn, $sql); + VisitorLogSchema::ensureIndexes($conn); + + if(isset($_GET['clear']) && $_GET['clear'] == "true") { + $conn->query("TRUNCATE `visitor_logs`"); $jsonResult["code"] = 0; $jsonResult["msg"] = "success"; $jsonResult["count"] = 0; $jsonResult["data"] = array(); - } else { // 默认分页数据 - // 获取当前页码(默认为第1页) - $pageNumber = isset($_GET['page']) ? (int) $_GET['page'] : 1; - - // 设置每页显示的记录条数 - $recordsPerPage = isset($_GET['limit']) ? (int) $_GET['limit'] : 50; - - // 计算起始位置 + } else { + $pageNumber = isset($_GET['page']) ? max(1, (int) $_GET['page']) : 1; + $recordsPerPage = isset($_GET['limit']) ? max(1, min(200, (int) $_GET['limit'])) : 50; $startPosition = ($pageNumber - 1) * $recordsPerPage; - $map = array(); - $where = 1; - - if(!empty($_GET['visit_date'])) { - $map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'"; + $where = VisitorLogListHelper::buildWhere($conn, $_GET); + + $sql = VisitorLogListHelper::listSelectSql($where, $startPosition, $recordsPerPage, false); + $result = $conn->query($sql); + $jsonData = array(); + + if ($result) { + while ($row = $result->fetch_assoc()) { + $jsonData[] = VisitorLogListHelper::formatListRow($row, $countries, $reasons); + } + $result->free(); } - $where .= cloak_visitor_log_apply_result_filter($conn, $_GET['result'] ?? ''); - - 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(!empty($map)) { - $where .= " AND " . join(" AND ", $map); - } - // SQL语句 - $sql = "SELECT `id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_hdata`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`, `device`, `add_time` FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}"; - - // 执行查询并输出结果 - $result = mysqli_query($conn, $sql); - $jsonData = array(); - - while ($row = mysqli_fetch_assoc($result)) { - //echo "

{$row["columnName"]}

"; // 根据需要修改列名 - if($row['vtimes'] == 0) { - $row['vtimes'] = 1; // 还未统计访问次数 - $sqlVtimes = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE `IP` = '" . cloak_db_escape($conn, $row["IP"]) . "' AND `domain` = '" . cloak_db_escape($conn, $row["domain"]) . "' AND id < " . (int)$row['id']); - $vtimesRow = mysqli_fetch_assoc($sqlVtimes); - $vtimes = $vtimesRow["total"]; // 如果之前此IP有访问记录 - if($vtimes > 0) { - $row['vtimes'] += $vtimes; // 访问次数叠加 - } - - // 更新访问次数 - $updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id']; - mysqli_query($conn, $updateSql); - } - - $row['country'] = $countries[$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'] ?? ''); - $row['fp_hdata_text'] = cloak_visitor_log_format_fp_hdata($row['fp_hdata'] ?? ''); - if (!empty($row['fp_hdata'])) { - $explain = cloak_visitor_log_fp_risk_explain( - $row['fp_hdata'], - $row['reason'] ?? '', - $row['result'] ?? '', - $row['domain'] ?? '' - ); - $row['fp_hdata_explain_summary'] = $explain['summary']; - $row['fp_hdata_explain_html'] = $explain['html']; - $row['fp_hdata_explain'] = $explain['data']; - } else { - $row['fp_hdata_explain_summary'] = ''; - $row['fp_hdata_explain_html'] = ''; - $row['fp_hdata_explain'] = null; - } - $row['api_by_request_text'] = VisitorApiAudit::displayText($row['api_by_request'] ?? ''); - $row['api_by_response_text'] = VisitorApiAudit::displayText($row['api_by_response'] ?? ''); - $row['api_risk_request_text'] = VisitorApiAudit::displayText($row['api_risk_request'] ?? ''); - $row['api_risk_response_text']= VisitorApiAudit::displayText($row['api_risk_response'] ?? ''); - $jsonData[] = $row; - } - - $sqlCount = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE {$where}"); - $countRow = mysqli_fetch_assoc($sqlCount); - $jsonResult["code"] = 0; $jsonResult["msg"] = ""; - $jsonResult["count"] = $countRow['total']; + $jsonResult["count"] = VisitorLogListHelper::countWhere($conn, $where); $jsonResult["data"] = $jsonData; } } - -// 关闭数据库连接 -mysqli_close($conn); -header('Content-Type: application/json'); +$conn->close(); -echo json_encode($jsonResult);exit; +header('Content-Type: application/json; charset=utf-8'); +echo json_encode($jsonResult); +exit; diff --git a/datatable/simulate.php b/datatable/simulate.php new file mode 100644 index 0000000..8c18602 --- /dev/null +++ b/datatable/simulate.php @@ -0,0 +1,62 @@ + false, 'message' => '未授权'], JSON_UNESCAPED_UNICODE); + exit; +} + +$action = $_REQUEST['action'] ?? ''; + +if ($action === 'prefill') { + $logId = (int) ($_GET['log_id'] ?? 0); + echo json_encode(VisitorSimulationRunner::prefillFromLog($logId), JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'configs') { + $list = []; + foreach (glob(dirname(__DIR__) . '/check_config/*_config.php') as $file) { + $name = basename($file, '_config.php'); + if ($name !== '') { + $list[] = $name; + } + } + sort($list); + echo json_encode(['ok' => true, 'configs' => $list], JSON_UNESCAPED_UNICODE); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['ok' => false, 'message' => '请使用 POST'], JSON_UNESCAPED_UNICODE); + exit; +} + +$payload = $_POST; +if (empty($payload) && ($raw = file_get_contents('php://input'))) { + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + $payload = $decoded; + } +} + +if ($action === 'replay') { + $logId = (int) ($payload['log_id'] ?? 0); + echo json_encode(VisitorSimulationRunner::replayFromLog($logId), JSON_UNESCAPED_UNICODE); + exit; +} + +if ($action === 'run') { + echo json_encode(VisitorSimulationRunner::runManual($payload), JSON_UNESCAPED_UNICODE); + exit; +} + +http_response_code(400); +echo json_encode(['ok' => false, 'message' => '未知 action'], JSON_UNESCAPED_UNICODE); diff --git a/f_check.php b/f_check.php index 07eecd0..7f3b52c 100755 --- a/f_check.php +++ b/f_check.php @@ -4,26 +4,27 @@ * 客户端指纹信息风险评估处理器 * 接收前端发送的指纹数据并进行综合风险判断 */ -session_start(); -// 设置响应头为JSON格式 -header('Content-Type: application/json; charset=utf-8'); +require_once __DIR__ . '/cong.php'; +require_once __DIR__ . '/lib/CloakHttpHelpers.php'; +require_once __DIR__ . '/lib/Cloak/CloakSession.php'; + +CloakFcheckCors::handlePreflight(); +CloakSession::start(); + +header('Content-Type: application/json; charset=utf-8'); +CloakFcheckCors::emitHeaders(); -// 确保请求方法为POST if ($_SERVER['REQUEST_METHOD'] !== 'POST') { http_response_code(405); echo json_encode([ - 'status' => 'error', - 'message' => '只允许POST请求' + 'status' => 'error', + 'message' => '只允许POST请求', ]); exit; } -// 获取并解析请求数据 $hdata = $_POST['hdata'] ?? ''; -$decodedString = base64_decode($hdata, true); -$fingerprint_info = is_string($decodedString) ? json_decode($decodedString, true) : null; - -if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) { +if ($hdata === '') { http_response_code(400); echo json_encode([ 'status' => 'error', @@ -32,170 +33,21 @@ if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) { exit; } -require_once __DIR__ . '/cong.php'; require_once __DIR__ . '/lib/DbHelper.php'; require_once __DIR__ . '/lib/bootstrap.php'; require_once __DIR__ . '/config/ConfigLoader.php'; -require_once __DIR__ . '/lib/Cloak/FpUrlHelper.php'; -require_once __DIR__ . '/lib/Cloak/VisitorApiAudit.php'; -require_once __DIR__ . '/lib/Cloak/RiskSecondaryGate.php'; +require_once __DIR__ . '/lib/Cloak/FCheckHandler.php'; -$fp_host = parse_url($fingerprint_info['domain'], PHP_URL_HOST); -if (empty($fp_host)) { - $fp_host = $_SERVER['HTTP_HOST'] ?? ''; -} -$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) { - $conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME); - if (!$conn->connect_error) { - require_once __DIR__ . '/lib/Cloak/VisitorLogSchema.php'; - VisitorLogSchema::ensureColumns($conn); - $resultEsc = cloak_db_escape($conn, 'true'); - $reasonEsc = cloak_db_escape($conn, '二次风控已关闭'); - $fpUrlEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($response['link']))); - $sql = "UPDATE `visitor_logs` SET `result`='{$resultEsc}',`reason`='{$reasonEsc}',`fp_url`='{$fpUrlEsc}' WHERE `id`='" . $log_id . "'"; - if ($conn->query($sql) !== true) { - $handle = fopen(dirname(__FILE__) . '/err.txt', 'a+'); - fwrite($handle, date('Y-m-d H:i:s') . ' | f_check risk disabled update: ' . $conn->error . "\n"); - fclose($handle); - } - $conn->close(); - } - } - - exit(json_encode($response)); -} - -$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)); -curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'forward_response_cookies'); -if ($_COOKIE) { - curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies()); -} -$return = curl_exec($ch); -if ($return === false) { - http_response_code(502); +$result = FCheckHandler::processFromHdata($hdata); +if (empty($result['ok'])) { + $status = (int) ($result['status'] ?? 400); + http_response_code($status); echo json_encode([ 'status' => 'error', - 'message' => 'Curl error: ' . curl_error($ch), - ]); - curl_close($ch); - exit; -} -curl_close($ch); -$return = json_decode($return, true); -if (!is_array($return)) { - http_response_code(502); - echo json_encode([ - 'status' => 'error', - 'message' => '风控 API 返回无效', + 'message' => $result['message'] ?? '处理失败', ]); exit; } -$_SESSION['visit_to_3'] = 3; -$response = []; -$response['reason'] = $return['reason']; -$response['result'] = $return['result']; - -if($return['result']) { - $_SESSION["check_result"] = "true"; // 通过指纹参数 - $resolved = FpUrlHelper::resolve($config_name, true); - $fp_url = $resolved['url']; - $response['link'] = $fp_url; -} else { - $_SESSION["check_result"] = "false"; // 未通过指纹参数 - if(CLOAK_REDIRECT_METHOD == 'curl') { - $response['link'] = ''; - } else { - $zp_url = DB_ZP; - $response['link'] = $zp_url; - } -} - -$update_log = []; -$update_log['result'] = $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 : ''); -} - -$servername = "localhost"; -$username = DB_USERNAME; -$password = DB_PASSWORD; -$dbname = DB_NAME; - -// Create connection -$conn = new mysqli($servername, $username, $password, $dbname); -// Check connection -if ($conn->connect_error) { - $error = "Connection failed: " . $conn->connect_error; - $handle = fopen(dirname(__FILE__) . "/err.txt", "a+"); - fwrite($handle, date("Y-m-d H:i:s") . ' | ' . $error . "\n"); - fclose($handle); -} elseif ($log_id > 0) { - $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, VisitorApiAudit::encode($jsonData)); - $apiRiskRespEsc = cloak_db_escape($conn, VisitorApiAudit::encode($return)); - require_once __DIR__ . '/lib/Cloak/VisitorLogSchema.php'; - VisitorLogSchema::ensureColumns($conn); - $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 . "'"; - - if ($conn->query($sql) != TRUE) { - $handle = fopen(dirname(__FILE__) . "/err.txt", "a+"); - fwrite($handle, date("Y-m-d H:i:s") . ' | ' . "Error: " . $sql . "
" . $conn->error . "\n"); - fclose($handle); - } - $conn->close(); -} - -// 记录日志 -exit(json_encode($response)); \ No newline at end of file +unset($result['ok'], $result['status'], $result['fingerprint_info'], $result['api_request'], $result['api_response']); +echo json_encode($result); diff --git a/indBXSXX.php b/indBXSXX.php new file mode 100644 index 0000000..0c861a7 --- /dev/null +++ b/indBXSXX.php @@ -0,0 +1,4 @@ += 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; + } +} diff --git a/lib/Cloak/CountryAllowlist.php b/lib/Cloak/CountryAllowlist.php old mode 100644 new mode 100755 index 47f2609..b1d88ad --- a/lib/Cloak/CountryAllowlist.php +++ b/lib/Cloak/CountryAllowlist.php @@ -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 diff --git a/lib/Cloak/CountryCodeNormalizer.php b/lib/Cloak/CountryCodeNormalizer.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/FCheckHandler.php b/lib/Cloak/FCheckHandler.php new file mode 100644 index 0000000..4f7b687 --- /dev/null +++ b/lib/Cloak/FCheckHandler.php @@ -0,0 +1,288 @@ + 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; + } +} diff --git a/lib/Cloak/FpRiskExplain.php b/lib/Cloak/FpRiskExplain.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/GeoIpCountryCache.php b/lib/Cloak/GeoIpCountryCache.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/GeoIpCountryResolver.php b/lib/Cloak/GeoIpCountryResolver.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/LogRiskAnalyzer.php b/lib/Cloak/LogRiskAnalyzer.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/Pipeline/RouteResolver.php b/lib/Cloak/Pipeline/RouteResolver.php index e1ed481..f036088 100755 --- a/lib/Cloak/Pipeline/RouteResolver.php +++ b/lib/Cloak/Pipeline/RouteResolver.php @@ -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 */ diff --git a/lib/Cloak/Pipeline/stages/check_country_geoip.inc.php b/lib/Cloak/Pipeline/stages/check_country_geoip.inc.php old mode 100644 new mode 100755 index dde832c..2b73099 --- a/lib/Cloak/Pipeline/stages/check_country_geoip.inc.php +++ b/lib/Cloak/Pipeline/stages/check_country_geoip.inc.php @@ -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, [ diff --git a/lib/Cloak/Pipeline/stages/check_url_args_timeout.inc.php b/lib/Cloak/Pipeline/stages/check_url_args_timeout.inc.php index 4691ad0..45b4e8f 100755 --- a/lib/Cloak/Pipeline/stages/check_url_args_timeout.inc.php +++ b/lib/Cloak/Pipeline/stages/check_url_args_timeout.inc.php @@ -1,5 +1,8 @@ 0) { if ($__cloak_debug_on) { cloak_dbg_step(__LINE__,'临时链接参数', 'info', 'CLOAK_URL_ARGS_TIMEOUT=' . CLOAK_URL_ARGS_TIMEOUT . ' 分钟,开始检查'); diff --git a/lib/Cloak/Pipeline/stages/run_main_api_fingerprint.inc.php b/lib/Cloak/Pipeline/stages/run_main_api_fingerprint.inc.php index d8e59f0..3c63716 100755 --- a/lib/Cloak/Pipeline/stages/run_main_api_fingerprint.inc.php +++ b/lib/Cloak/Pipeline/stages/run_main_api_fingerprint.inc.php @@ -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); } diff --git a/lib/Cloak/RiskSecondaryCache.php b/lib/Cloak/RiskSecondaryCache.php new file mode 100644 index 0000000..2391fbe --- /dev/null +++ b/lib/Cloak/RiskSecondaryCache.php @@ -0,0 +1,108 @@ + $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, + ]; + } +} diff --git a/lib/Cloak/RiskSecondaryGate.php b/lib/Cloak/RiskSecondaryGate.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/SimulationRouteResolver.php b/lib/Cloak/SimulationRouteResolver.php new file mode 100644 index 0000000..10fb966 --- /dev/null +++ b/lib/Cloak/SimulationRouteResolver.php @@ -0,0 +1,149 @@ + '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; + } +} diff --git a/lib/Cloak/VisitorApiAudit.php b/lib/Cloak/VisitorApiAudit.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/VisitorLogAnalytics.php b/lib/Cloak/VisitorLogAnalytics.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/VisitorLogListHelper.php b/lib/Cloak/VisitorLogListHelper.php new file mode 100644 index 0000000..573f7b2 --- /dev/null +++ b/lib/Cloak/VisitorLogListHelper.php @@ -0,0 +1,135 @@ += '{$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); + } +} diff --git a/lib/Cloak/VisitorLogQueue.php b/lib/Cloak/VisitorLogQueue.php old mode 100644 new mode 100755 diff --git a/lib/Cloak/VisitorLogSchema.php b/lib/Cloak/VisitorLogSchema.php index 2af4943..fe8e5fe 100755 --- a/lib/Cloak/VisitorLogSchema.php +++ b/lib/Cloak/VisitorLogSchema.php @@ -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)) { diff --git a/lib/Cloak/VisitorLogVtimes.php b/lib/Cloak/VisitorLogVtimes.php new file mode 100644 index 0000000..63f950b --- /dev/null +++ b/lib/Cloak/VisitorLogVtimes.php @@ -0,0 +1,41 @@ +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; + } +} diff --git a/lib/Cloak/VisitorLogWorker.php b/lib/Cloak/VisitorLogWorker.php old mode 100644 new mode 100755 index cb5ef7f..126c13f --- a/lib/Cloak/VisitorLogWorker.php +++ b/lib/Cloak/VisitorLogWorker.php @@ -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 $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 */ diff --git a/lib/Cloak/VisitorRepository.php b/lib/Cloak/VisitorRepository.php index 0636855..3f7acd4 100755 --- a/lib/Cloak/VisitorRepository.php +++ b/lib/Cloak/VisitorRepository.php @@ -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(); diff --git a/lib/Cloak/VisitorSimulationRunner.php b/lib/Cloak/VisitorSimulationRunner.php new file mode 100644 index 0000000..7eb8757 --- /dev/null +++ b/lib/Cloak/VisitorSimulationRunner.php @@ -0,0 +1,591 @@ + 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; + } + } + } +} diff --git a/lib/Cloak/iso3166_maps.php b/lib/Cloak/iso3166_maps.php old mode 100644 new mode 100755 diff --git a/lib/InstallSchema.php b/lib/InstallSchema.php index 4e98356..37d7427 100755 --- a/lib/InstallSchema.php +++ b/lib/InstallSchema.php @@ -67,6 +67,8 @@ class InstallSchema `api_by_response` text DEFAULT NULL COMMENT 'byApi 响应 JSON', `api_risk_request` text DEFAULT NULL COMMENT 'byApiRisk 请求参数 JSON', `api_risk_response` text DEFAULT NULL COMMENT 'byApiRisk 响应 JSON', + `is_simulation` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否模拟测试', + `sim_source_log_id` int(11) UNSIGNED DEFAULT NULL COMMENT '模拟重放来源日志 ID', `add_time` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '日志记录时间', PRIMARY KEY (`id`), KEY `IP` (`IP`), diff --git a/lib/bootstrap.php b/lib/bootstrap.php index 4f2fb97..d24f2ce 100755 --- a/lib/bootstrap.php +++ b/lib/bootstrap.php @@ -26,6 +26,7 @@ require_once __DIR__ . '/Cloak/GeoIpCountryResolver.php'; require_once __DIR__ . '/Cloak/CountryCodeNormalizer.php'; require_once __DIR__ . '/Cloak/CountryAllowlist.php'; require_once __DIR__ . '/Cloak/RiskSecondaryGate.php'; +require_once __DIR__ . '/Cloak/RiskSecondaryCache.php'; require_once __DIR__ . '/Cloak/VisitorLogSchema.php'; require_once __DIR__ . '/Cloak/VisitorApiAudit.php'; require_once __DIR__ . '/Cloak/VisitorLogQueue.php'; diff --git a/log_analytics.php b/log_analytics.php old mode 100644 new mode 100755 diff --git a/resources/virtual_device_detect.inc.php b/resources/virtual_device_detect.inc.php old mode 100644 new mode 100755 diff --git a/simulate_test.php b/simulate_test.php new file mode 100644 index 0000000..0e48cdb --- /dev/null +++ b/simulate_test.php @@ -0,0 +1,237 @@ +alert('密码错误!');window.location='table.php'; "); + exit; +} + +$fromLog = (int) ($_GET['from_log'] ?? 0); +$configList = []; +foreach (glob(__DIR__ . '/check_config/*_config.php') as $file) { + $name = basename($file, '_config.php'); + if ($name !== '') { + $configList[] = $name; + } +} +sort($configList); + +function sim_h($s) +{ + return htmlspecialchars((string) $s, ENT_QUOTES, 'UTF-8'); +} +?> + + + + + 模拟测试访问 + + + + + +
+ + +
此操作会调用真实 byApi / byApiRisk 接口,并写入 visitor_logs(标记为模拟测试)。
+ +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ 高级选项(Cookie / 二次风控 fp_hdata) +
+
+ + +
+
+ + +
+
+
+
+ + 0): ?> + + + 返回日志 +
+
+
+
+
+ + + + + + diff --git a/storage/runtime/c7f2a9e1/.htaccess b/storage/runtime/c7f2a9e1/.htaccess old mode 100644 new mode 100755 diff --git a/storage/runtime/c7f2a9e1/cc_cache/838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5.json b/storage/runtime/c7f2a9e1/cc_cache/838c4c2573848f58e74332341a7ca6bc5cd86a8aec7d644137d53b4d597f10f5.json old mode 100644 new mode 100755 diff --git a/storage/visitor_log_queue/.gitkeep b/storage/visitor_log_queue/.gitkeep old mode 100644 new mode 100755 diff --git a/storage/visitor_log_queue/pending.ndjson b/storage/visitor_log_queue/pending.ndjson old mode 100644 new mode 100755 diff --git a/table.php b/table.php index 963aaad..bcfc580 100755 --- a/table.php +++ b/table.php @@ -348,6 +348,7 @@ if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
访问日志 + 模拟测试 数据分析 配置中心
@@ -406,6 +407,7 @@ if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
+ 模拟测试 当前查询数量: @@ -509,7 +511,13 @@ layui.use(function(){ } } - function cellApiAudit(val, title) { + function cellApiAudit(val, title, d, field) { + if (val === '__LAZY__' && d && d.id && field) { + return '
' + + '已记录' + + '查看 JSON' + + '
'; + } if (val == null || val === '' || val === '-') { return ''; } @@ -522,30 +530,23 @@ layui.use(function(){ } function cellFpHdata(d) { - if (!d.fp_hdata && !d.fp_hdata_text) { + if (!d.fp_hdata_text) { return ''; } - var summary = d.fp_hdata_explain_summary || d.fp_hdata_text || ''; - var html = ''; - html += '
'; + var summary = d.fp_hdata_text || ''; + var html = '
'; if (summary) { html += '' + escapeHtml(summary.length > 48 ? summary.slice(0, 48) + '…' : summary) + ''; } html += ''; - if (d.fp_hdata_explain_html) { - html += '判定详情'; - window.__fpExplainCache = window.__fpExplainCache || {}; - window.__fpExplainCache[d.id] = d.fp_hdata_explain_html; - } - if (d.fp_hdata || d.fp_hdata_text) { - html += '原始 hdata'; - } + html += '判定详情'; + html += '原始 hdata'; html += '
'; return html; } var COLUMN_DEFS = [ - { field: 'id', title: 'ID', width: 56, fixed: true, defaultVisible: true }, + { field: 'id', title: 'ID', width: 56, fixed: 'left', defaultVisible: true }, { field: 'domain', title: '域名', width: 120, defaultVisible: true, templet: function(d){ return cellEllipsis(d.domain, 18); } }, { field: 'visit_date', title: '访问日期', width: 130, defaultVisible: true, templet: function(d){ return '' + escapeHtml(d.visit_date) + ''; @@ -570,10 +571,20 @@ layui.use(function(){ { field: 'http_referer', title: 'HTTP-Referer', width: 260, defaultVisible: false, templet: function(d){ return cellLinkEllipsis(d.http_referer, LINK_CELL_MAX); } }, { field: 'accept_language_raw', title: 'Accept-Language', width: 220, defaultVisible: false, templet: function(d){ return cellEllipsis(d.accept_language_raw, 32); } }, { field: 'fp_hdata_text', title: '指纹 hdata', width: 360, defaultVisible: false, templet: function(d){ return cellFpHdata(d); } }, - { field: 'api_by_request_text', title: 'byApi 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_request_text, 'byApi 请求参数 #' + d.id); } }, - { field: 'api_by_response_text', title: 'byApi 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_response_text, 'byApi 返回值 #' + d.id); } }, - { field: 'api_risk_request_text', title: 'byApiRisk 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_request_text, 'byApiRisk 请求参数 #' + d.id); } }, - { field: 'api_risk_response_text', title: 'byApiRisk 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_response_text, 'byApiRisk 返回值 #' + d.id); } }, + { field: 'api_by_request_text', title: 'byApi 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_request_text, 'byApi 请求参数 #' + d.id, d, 'api_by_request'); } }, + { field: 'api_by_response_text', title: 'byApi 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_by_response_text, 'byApi 返回值 #' + d.id, d, 'api_by_response'); } }, + { field: 'api_risk_request_text', title: 'byApiRisk 请求参数', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_request_text, 'byApiRisk 请求参数 #' + d.id, d, 'api_risk_request'); } }, + { field: 'api_risk_response_text', title: 'byApiRisk 返回值', width: 300, defaultVisible: false, templet: function(d){ return cellApiAudit(d.api_risk_response_text, 'byApiRisk 返回值 #' + d.id, d, 'api_risk_response'); } }, + { field: 'sim_tag', title: '模拟', width: 88, defaultVisible: true, templet: function(d){ + if (String(d.is_simulation) === '1') { + var src = d.sim_source_log_id ? ' #' + d.sim_source_log_id : ''; + return '是' + escapeHtml(String(src)) + ''; + } + return ''; + }}, + { field: 'sim_action', title: '操作', width: 108, defaultVisible: true, templet: function(d){ + return '模拟测试'; + }}, ]; var COLUMN_DEF_MAP = {}; @@ -681,14 +692,35 @@ layui.use(function(){ }); } + function applyTableRowStyles(res) { + if (!res || !Array.isArray(res.data)) { + return; + } + var $rows = $('.log-table-wrap .layui-table-body table tbody tr'); + res.data.forEach(function(row, idx) { + var $tr = $rows.eq(idx); + if (!$tr.length) { + return; + } + if (row.reason && String(row.reason).indexOf('未确定流量来源') !== -1) { + $tr.css({ color: '#856404', backgroundColor: '#fff8e6' }); + } + }); + } + + function onTableDataRendered(res, count) { + if (typeof count === 'number') { + $("#total_nums").text(count); + } + applyTableRowStyles(res); + fixRichTableCellHeights(); + } + function reloadTableCols() { table.reload(TABLE_ID, { cols: buildCols(colConfig), done: function(res, curr, count) { - if (typeof count === 'number') { - $("#total_nums").text(count); - } - fixRichTableCellHeights(); + onTableDataRendered(res, count); } }); } @@ -822,20 +854,8 @@ layui.use(function(){ size: "sm", skin: "line", even: true, - rowStyle: function(row) { - if (row.reason && row.reason.indexOf('未确定流量来源') !== -1) { - return 'color:#856404;background-color:#fff8e6;'; - } - }, - lineStyle: function(row) { - if (row.fp_hdata || row.fp_hdata_text || row.fp_hdata_explain_html) { - return 'height:auto;'; - } - return ''; - }, done: function(res, curr, count, origin){ - $("#total_nums").text(count); - fixRichTableCellHeights(); + onTableDataRendered(res, count); }, }); @@ -844,28 +864,80 @@ layui.use(function(){ $(document).on('click', '.fp-hdata-btn', function(e) { e.stopPropagation(); var id = this.getAttribute('data-explain-id'); - if (id && window.__fpExplainCache && window.__fpExplainCache[id]) { - layer.open({ - type: 1, - title: '二次风控判定解析 #' + id, - area: ['720px', '85vh'], - shadeClose: true, - content: '
' + window.__fpExplainCache[id] + '
' + var logId = this.getAttribute('data-log-id'); + var field = this.getAttribute('data-field'); + var full = this.getAttribute('data-full') || ''; + + if (id) { + if (window.__fpExplainCache && window.__fpExplainCache[id]) { + layer.open({ + type: 1, + title: '二次风控判定解析 #' + id, + area: ['720px', '85vh'], + shadeClose: true, + content: '
' + window.__fpExplainCache[id] + '
' + }); + return; + } + var loadIdx = layer.load(1); + $.getJSON('datatable/fp_explain.php', { id: id }, function(res) { + layer.close(loadIdx); + if (!res || res.code !== 0 || !res.html) { + layer.msg((res && res.msg) ? res.msg : '加载判定详情失败'); + return; + } + window.__fpExplainCache = window.__fpExplainCache || {}; + window.__fpExplainCache[id] = res.html; + layer.open({ + type: 1, + title: '二次风控判定解析 #' + id, + area: ['720px', '85vh'], + shadeClose: true, + content: '
' + res.html + '
' + }); + }).fail(function() { + layer.close(loadIdx); + layer.msg('加载判定详情失败'); }); return; } - var full = this.getAttribute('data-full') || ''; + + if (logId && field) { + var popupTitle = this.getAttribute('data-title') || (field === 'fp_hdata' ? '指纹 hdata' : field); + var isJson = this.classList.contains('fp-hdata-btn-json') || field.indexOf('api_') === 0 || field === 'fp_hdata'; + var loadIdx2 = layer.load(1); + $.getJSON('datatable/log_blob.php', { id: logId, field: field }, function(res) { + layer.close(loadIdx2); + if (!res || res.code !== 0 || !res.data) { + layer.msg((res && res.msg) ? res.msg : '加载失败'); + return; + } + var body = isJson ? escapeHtml(formatJsonPretty(res.data)) : escapeHtml(res.data); + layer.open({ + type: 1, + title: popupTitle + ' #' + logId, + area: ['680px', '70vh'], + shadeClose: true, + content: '
' + body + '
' + }); + }).fail(function() { + layer.close(loadIdx2); + layer.msg('加载失败'); + }); + return; + } + if (full) { - var popupTitle = this.getAttribute('data-title') || '指纹 hdata'; - var body = this.classList.contains('fp-hdata-btn-json') + var popupTitleStatic = this.getAttribute('data-title') || '指纹 hdata'; + var bodyStatic = this.classList.contains('fp-hdata-btn-json') ? escapeHtml(formatJsonPretty(full)) : escapeHtml(full); layer.open({ type: 1, - title: popupTitle, + title: popupTitleStatic, area: ['680px', '70vh'], shadeClose: true, - content: '
' + body + '
' + content: '
' + bodyStatic + '
' }); } }); diff --git a/tools/regression_test.php b/tools/regression_test.php index 4e4eca5..65bc749 100755 --- a/tools/regression_test.php +++ b/tools/regression_test.php @@ -230,6 +230,14 @@ $ntVerifyOut = shell_exec($ntVerifyCmd); $ntVerifyJson = json_decode((string) $ntVerifyOut, true); reg_assert('f_check nt.txt 畸形行跳过且 fp 索引合法', ($ntVerifyJson['ok'] ?? false) === true, (string) $ntVerifyOut); +$riskCacheOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_risk_cache.php') . ' 2>/dev/null'); +$riskCacheJson = json_decode((string) $riskCacheOut, true); +reg_assert('二次风控 Session 缓存与 CORS', ($riskCacheJson['ok'] ?? false) === true, (string) $riskCacheOut); + +$datalistHelperOut = shell_exec(escapeshellarg(PHP_BINARY) . ' ' . escapeshellarg($root . '/tools/verify_datalist_helper.php') . ' 2>/dev/null'); +$datalistHelperJson = json_decode((string) $datalistHelperOut, true); +reg_assert('日志列表 VisitorLogListHelper', ($datalistHelperJson['ok'] ?? false) === true, (string) $datalistHelperOut); + // ── 12. ConfigLoader / 宝塔部署文件 ─────────────────────────── echo "[12] ConfigLoader 与宝塔部署\n"; require_once $root . '/config/ConfigLoader.php'; @@ -322,6 +330,24 @@ if (defined('DB_NAME')) { if ($waitId) { $conn->query('DELETE FROM `visitor_logs` WHERE `id` = ' . (int) $waitId); } + + $GLOBALS['__cloak_force_sync_log'] = true; + $simLogId = @write_log_db(array_merge($sampleLogs, [ + 'visit_md5_code' => 'reg_sim_' . substr(md5((string) microtime(true)), 0, 12), + 'is_simulation' => 1, + 'sim_source_log_id' => 99999, + 'reason' => VisitorSimulationRunner::tagReason('regression'), + ])); + unset($GLOBALS['__cloak_force_sync_log']); + reg_assert('模拟日志 is_simulation 写入', $simLogId !== null && (int) $simLogId > 0); + if ($simLogId) { + $chk = $conn->query('SELECT is_simulation, sim_source_log_id FROM visitor_logs WHERE id=' . (int) $simLogId); + $simRow = $chk ? $chk->fetch_assoc() : null; + reg_assert('模拟列 is_simulation=1', $simRow && (int) $simRow['is_simulation'] === 1); + reg_assert('模拟列 sim_source_log_id', $simRow && (int) $simRow['sim_source_log_id'] === 99999); + $conn->query('DELETE FROM visitor_logs WHERE id=' . (int) $simLogId); + } + $conn->close(); } } else { @@ -335,6 +361,17 @@ CloakDebugPage::init(); CloakDebugPage::stepAt(__LINE__, 'regression', 'info', 'ok'); reg_assert('CloakDebugPage::stepAt 可调用', true); +// ── 11. 模拟测试组件 ───────────────────────────────────────── +echo "[11] 模拟测试组件\n"; +require_once $root . '/lib/Cloak/VisitorSimulationRunner.php'; +reg_assert('FCheckHandler 已加载', class_exists('FCheckHandler', false)); +reg_assert('SimulationRouteResolver 已加载', class_exists('SimulationRouteResolver', false)); +reg_assert('VisitorSimulationRunner 已加载', class_exists('VisitorSimulationRunner', false)); +$tagged = VisitorSimulationRunner::tagReason('国家不符'); +reg_assert('VisitorSimulationRunner::tagReason 前缀', strpos($tagged, '[模拟测试]') === 0, $tagged); +$badPrefill = VisitorSimulationRunner::prefillFromLog(0); +reg_assert('prefillFromLog(0) 返回失败', empty($badPrefill['ok'])); + // ── 汇总 ────────────────────────────────────────────────────── echo "\n=== 汇总 ===\n"; echo "通过: {$passed} 失败: {$failed} 跳过: {$skipped}\n"; diff --git a/tools/setup_geoip_db.php b/tools/setup_geoip_db.php old mode 100644 new mode 100755 diff --git a/tools/verify_config_loader.php b/tools/verify_config_loader.php index e7f86ec..f6f37db 100755 --- a/tools/verify_config_loader.php +++ b/tools/verify_config_loader.php @@ -12,6 +12,16 @@ $tests['normalize_strips_www'] = DomainRepository::normalizeHostname('www.Shop.E $tests['host_to_key'] = DomainRepository::hostToConfigKey('shop.example.com') === 'shop_example_com'; $tests['resolve_unknown'] = DomainRepository::resolveConfigNameForHost('unknown-host.test', $root) === 'index'; +$key = 'reg_script_' . substr(md5((string) microtime(true)), 0, 6); +$cfgFile = $root . '/check_config/' . $key . '_config.php'; +file_put_contents($cfgFile, "connect_error) { + echo json_encode(['ok' => false, 'error' => 'db'], JSON_UNESCAPED_UNICODE) . "\n"; + exit(1); +} + +$where = VisitorLogListHelper::buildWhere($conn, ['visit_date' => '2026-01-15']); +$tests['date_range_where'] = strpos($where, 'visit_date') !== false && strpos($where, 'DATE_FORMAT') === false; + +$sql = VisitorLogListHelper::listSelectSql('1', 0, 10, true); +$tests['lean_select_no_fp_hdata'] = strpos($sql, '`fp_hdata`,') === false && strpos($sql, 'has_fp_hdata') !== false; + +$row = VisitorLogListHelper::formatListRow([ + 'country' => 'US', + 'reason' => 'test', + 'result' => 'wait', + 'judge_timing' => '', + 'has_fp_hdata' => 1, + 'has_api_by_request' => 1, +], ['US' => '美国'], []); + +$tests['format_wait'] = $row['result'] === '检测中'; +$tests['format_lazy_api'] = $row['api_by_request_text'] === '__LAZY__'; +$tests['format_fp_flag'] = $row['fp_hdata_text'] === '有指纹数据'; +$tests['no_explain_html'] = !array_key_exists('fp_hdata_explain_html', $row); + +$conn->close(); + +$ok = !in_array(false, $tests, true); +echo json_encode(['ok' => $ok, 'tests' => $tests], JSON_UNESCAPED_UNICODE) . "\n"; +exit($ok ? 0 : 1); diff --git a/tools/verify_geoip.php b/tools/verify_geoip.php old mode 100644 new mode 100755 index 808a738..4c54426 --- a/tools/verify_geoip.php +++ b/tools/verify_geoip.php @@ -55,7 +55,10 @@ if ($tests['resolver_available']) { $tests['normalize_iso2'] = CountryCodeNormalizer::toAlpha2('us') === 'US'; $tests['normalize_name'] = CountryCodeNormalizer::toAlpha2('United States') === 'US'; $tests['allowlist_name'] = CountryAllowlist::isAllowed('United Kingdom', CountryAllowlist::parse('GB,US')); - if (!$tests['normalize_iso2'] || !$tests['normalize_name'] || !$tests['allowlist_name']) { + $tests['allowlist_fullwidth_comma'] = CountryAllowlist::isAllowed('BR', CountryAllowlist::parse('BR,CN')); + $tests['allowlist_country_name'] = CountryAllowlist::isAllowed('BR', CountryAllowlist::parse('Brazil,China')); + if (!$tests['normalize_iso2'] || !$tests['normalize_name'] || !$tests['allowlist_name'] + || !$tests['allowlist_fullwidth_comma'] || !$tests['allowlist_country_name']) { $ok = false; } } diff --git a/tools/verify_risk_cache.php b/tools/verify_risk_cache.php new file mode 100644 index 0000000..1f5c282 --- /dev/null +++ b/tools/verify_risk_cache.php @@ -0,0 +1,64 @@ +#!/usr/bin/env php + '203.0.113.10', + 'result' => 'true', + 'reason' => 'ok', + 'fp_url' => 'https://fp.example/a', +]); +$hit = RiskSecondaryCache::get('203.0.113.10'); +$tests['same_ip_hit'] = is_array($hit) && ($hit['result'] ?? '') === 'true'; + +RiskSecondaryCache::applyToSession($hit); +$tests['apply_session'] = ($_SESSION['visit_to_3'] ?? '') == 3 && ($_SESSION['check_result'] ?? '') === 'true'; + +$miss = RiskSecondaryCache::get('198.51.100.1'); +$tests['ip_change_miss'] = $miss === null && empty($_SESSION[RiskSecondaryCache::SESSION_KEY]); +$tests['ip_change_resets_visit_to_3'] = ($_SESSION['visit_to_3'] ?? '') === '1'; + +RiskSecondaryCache::store([ + 'ip' => '203.0.113.10', + 'result' => 'false', + 'reason' => 'deny', + 'fp_url' => 'https://zp.example/', +]); +$api = RiskSecondaryCache::toApiResponse(RiskSecondaryCache::get('203.0.113.10')); +$tests['to_api_response'] = $api['result'] === false && $api['reason'] === RiskSecondaryCache::hitReason(); + +RiskSecondaryCache::clear(); +$tests['clear'] = RiskSecondaryCache::get('203.0.113.10') === null; + +$originOk = false; +$_SERVER['HTTP_HOST'] = 'cloak.example.com'; +$_SERVER['HTTP_ORIGIN'] = 'https://www.example.com'; +require_once $root . '/lib/Cloak/CloakSession.php'; +$ref = new ReflectionClass('CloakFcheckCors'); +$method = $ref->getMethod('isAllowedOrigin'); +$method->setAccessible(true); +$originOk = (bool) $method->invoke(null, 'https://www.example.com'); +$tests['cors_sibling_subdomain'] = $originOk; + +$badOrigin = !(bool) $method->invoke(null, 'https://evil.other.com'); +$tests['cors_reject_foreign'] = $badOrigin; + +$ok = !in_array(false, $tests, true); + +echo json_encode(['ok' => $ok, 'tests' => $tests], JSON_UNESCAPED_UNICODE) . "\n"; +exit($ok ? 0 : 1); diff --git a/tools/verify_virtual_detect.php b/tools/verify_virtual_detect.php old mode 100644 new mode 100755 diff --git a/tools/visitor_log_worker.php b/tools/visitor_log_worker.php old mode 100644 new mode 100755 diff --git a/vendor/autoload.php b/vendor/autoload.php old mode 100644 new mode 100755 diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php old mode 100644 new mode 100755 diff --git a/vendor/composer/InstalledVersions.php b/vendor/composer/InstalledVersions.php old mode 100644 new mode 100755 diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE old mode 100644 new mode 100755 diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php old mode 100644 new mode 100755 diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php old mode 100644 new mode 100755 diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php old mode 100644 new mode 100755 diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php old mode 100644 new mode 100755 diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php old mode 100644 new mode 100755 diff --git a/vendor/composer/ca-bundle/LICENSE b/vendor/composer/ca-bundle/LICENSE old mode 100644 new mode 100755 diff --git a/vendor/composer/ca-bundle/README.md b/vendor/composer/ca-bundle/README.md old mode 100644 new mode 100755 diff --git a/vendor/composer/ca-bundle/composer.json b/vendor/composer/ca-bundle/composer.json old mode 100644 new mode 100755 diff --git a/vendor/composer/ca-bundle/res/cacert.pem b/vendor/composer/ca-bundle/res/cacert.pem old mode 100644 new mode 100755 diff --git a/vendor/composer/ca-bundle/src/CaBundle.php b/vendor/composer/ca-bundle/src/CaBundle.php old mode 100644 new mode 100755 diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json old mode 100644 new mode 100755 diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php old mode 100644 new mode 100755 diff --git a/vendor/composer/platform_check.php b/vendor/composer/platform_check.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/CHANGELOG.md b/vendor/geoip2/geoip2/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/LICENSE b/vendor/geoip2/geoip2/LICENSE old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/README.md b/vendor/geoip2/geoip2/README.md old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/composer.json b/vendor/geoip2/geoip2/composer.json old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/examples/benchmark.php b/vendor/geoip2/geoip2/examples/benchmark.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Database/Reader.php b/vendor/geoip2/geoip2/src/Database/Reader.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/AddressNotFoundException.php b/vendor/geoip2/geoip2/src/Exception/AddressNotFoundException.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/AuthenticationException.php b/vendor/geoip2/geoip2/src/Exception/AuthenticationException.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/GeoIp2Exception.php b/vendor/geoip2/geoip2/src/Exception/GeoIp2Exception.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/HttpException.php b/vendor/geoip2/geoip2/src/Exception/HttpException.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/InvalidRequestException.php b/vendor/geoip2/geoip2/src/Exception/InvalidRequestException.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Exception/OutOfQueriesException.php b/vendor/geoip2/geoip2/src/Exception/OutOfQueriesException.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/AbstractModel.php b/vendor/geoip2/geoip2/src/Model/AbstractModel.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/AnonymousIp.php b/vendor/geoip2/geoip2/src/Model/AnonymousIp.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Asn.php b/vendor/geoip2/geoip2/src/Model/Asn.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/City.php b/vendor/geoip2/geoip2/src/Model/City.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/ConnectionType.php b/vendor/geoip2/geoip2/src/Model/ConnectionType.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Country.php b/vendor/geoip2/geoip2/src/Model/Country.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Domain.php b/vendor/geoip2/geoip2/src/Model/Domain.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Enterprise.php b/vendor/geoip2/geoip2/src/Model/Enterprise.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Insights.php b/vendor/geoip2/geoip2/src/Model/Insights.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Model/Isp.php b/vendor/geoip2/geoip2/src/Model/Isp.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/ProviderInterface.php b/vendor/geoip2/geoip2/src/ProviderInterface.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/AbstractPlaceRecord.php b/vendor/geoip2/geoip2/src/Record/AbstractPlaceRecord.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/AbstractRecord.php b/vendor/geoip2/geoip2/src/Record/AbstractRecord.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/City.php b/vendor/geoip2/geoip2/src/Record/City.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Continent.php b/vendor/geoip2/geoip2/src/Record/Continent.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Country.php b/vendor/geoip2/geoip2/src/Record/Country.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Location.php b/vendor/geoip2/geoip2/src/Record/Location.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/MaxMind.php b/vendor/geoip2/geoip2/src/Record/MaxMind.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Postal.php b/vendor/geoip2/geoip2/src/Record/Postal.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/RepresentedCountry.php b/vendor/geoip2/geoip2/src/Record/RepresentedCountry.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Subdivision.php b/vendor/geoip2/geoip2/src/Record/Subdivision.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Record/Traits.php b/vendor/geoip2/geoip2/src/Record/Traits.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/Util.php b/vendor/geoip2/geoip2/src/Util.php old mode 100644 new mode 100755 diff --git a/vendor/geoip2/geoip2/src/WebService/Client.php b/vendor/geoip2/geoip2/src/WebService/Client.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/CHANGELOG.md b/vendor/maxmind-db/reader/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/LICENSE b/vendor/maxmind-db/reader/LICENSE old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/README.md b/vendor/maxmind-db/reader/README.md old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/autoload.php b/vendor/maxmind-db/reader/autoload.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/composer.json b/vendor/maxmind-db/reader/composer.json old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/config.m4 b/vendor/maxmind-db/reader/ext/config.m4 old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/config.w32 b/vendor/maxmind-db/reader/ext/config.w32 old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/maxminddb.c b/vendor/maxmind-db/reader/ext/maxminddb.c old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/php_maxminddb.h b/vendor/maxmind-db/reader/ext/php_maxminddb.h old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/tests/001-load.phpt b/vendor/maxmind-db/reader/ext/tests/001-load.phpt old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/tests/002-final.phpt b/vendor/maxmind-db/reader/ext/tests/002-final.phpt old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt b/vendor/maxmind-db/reader/ext/tests/003-open-basedir.phpt old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/package.xml b/vendor/maxmind-db/reader/package.xml old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Decoder.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/InvalidDatabaseException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Metadata.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php b/vendor/maxmind-db/reader/src/MaxMind/Db/Reader/Util.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/CHANGELOG.md b/vendor/maxmind/web-service-common/CHANGELOG.md old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/LICENSE b/vendor/maxmind/web-service-common/LICENSE old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/README.md b/vendor/maxmind/web-service-common/README.md old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/composer.json b/vendor/maxmind/web-service-common/composer.json old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/phpstan.neon b/vendor/maxmind/web-service-common/phpstan.neon old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/AuthenticationException.php b/vendor/maxmind/web-service-common/src/Exception/AuthenticationException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/HttpException.php b/vendor/maxmind/web-service-common/src/Exception/HttpException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/InsufficientFundsException.php b/vendor/maxmind/web-service-common/src/Exception/InsufficientFundsException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/InvalidInputException.php b/vendor/maxmind/web-service-common/src/Exception/InvalidInputException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/InvalidRequestException.php b/vendor/maxmind/web-service-common/src/Exception/InvalidRequestException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php b/vendor/maxmind/web-service-common/src/Exception/IpAddressNotFoundException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/PermissionRequiredException.php b/vendor/maxmind/web-service-common/src/Exception/PermissionRequiredException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/Exception/WebServiceException.php b/vendor/maxmind/web-service-common/src/Exception/WebServiceException.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/WebService/Client.php b/vendor/maxmind/web-service-common/src/WebService/Client.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/WebService/Http/CurlRequest.php b/vendor/maxmind/web-service-common/src/WebService/Http/CurlRequest.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/WebService/Http/Request.php b/vendor/maxmind/web-service-common/src/WebService/Http/Request.php old mode 100644 new mode 100755 diff --git a/vendor/maxmind/web-service-common/src/WebService/Http/RequestFactory.php b/vendor/maxmind/web-service-common/src/WebService/Http/RequestFactory.php old mode 100644 new mode 100755