修复日志页面卡顿我呢提
This commit is contained in:
@@ -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'];
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
define('SHOW_SITE_MODE_SWITCH', 'ip_check' );
|
||||
define('SHOW_SITE_IP', '' );
|
||||
define('BLACKLIST_GROUP_ID', 0 );
|
||||
define('WHITELIST_GROUP_ID', 0 );
|
||||
define('SHOW_SITE_COUNTRY', 'BR' );
|
||||
define('CLOAK_SHOW_CONTENT', '302');
|
||||
define('DB_FP', array (
|
||||
0 => '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');
|
||||
?>
|
||||
+1
-1
@@ -58,7 +58,7 @@ if ($__cloak_risk_enhanced) {
|
||||
|
||||
/* ===== 指纹采集 + 提交(轻量字段始终采集;耗时采集仅加强判断 ON) ===== */
|
||||
(async function () {
|
||||
const __cloakRiskEnhanced = <?php echo $__cloak_risk_enhanced ? 'true' : 'false'; ?>;
|
||||
const __cloakRiskEnhanced = <?php echo $__cloak_risk_enhanced ? 1 : 0; ?>;
|
||||
try {
|
||||
let _0x3b0528;
|
||||
let _0x12fad4;
|
||||
|
||||
Regular → Executable
+10
-1
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -890,7 +890,7 @@ $_d = function($name, $default) { return defined($name) ? constant($name) : $def
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="form-label-cell">指定国家访问真实页(逗号分隔国家代码)</td>
|
||||
<td class="form-val-cell"><input type="text" name="country" class="form-control form-control-sm" value="<?php echo SHOW_SITE_COUNTRY; ?>" placeholder="如:US,GB,CA" style="max-width:300px;"></td>
|
||||
<td class="form-val-cell"><input type="text" name="country" class="form-control form-control-sm" value="<?php echo SHOW_SITE_COUNTRY; ?>" placeholder="如:US,GB,CA(请用英文逗号)" style="max-width:300px;"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="form-label-cell">广告策略 ID</td>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* 日志 datatable 接口鉴权
|
||||
*/
|
||||
if (!function_exists('cloak_datatable_require_auth')) {
|
||||
function cloak_datatable_require_auth()
|
||||
{
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
unset($_SESSION['password']);
|
||||
unset($_SESSION['check_result']);
|
||||
http_response_code(404);
|
||||
die('404 Not Found');
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-116
@@ -4,8 +4,10 @@ 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");
|
||||
session_start();
|
||||
require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogListHelper.php");
|
||||
require_once(dirname(__DIR__) . "/datatable/auth.php");
|
||||
|
||||
cloak_datatable_require_auth();
|
||||
|
||||
$reasons = array(
|
||||
"blank_referrer" => "过滤无来源的访问",
|
||||
@@ -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`";
|
||||
VisitorLogSchema::ensureIndexes($conn);
|
||||
|
||||
// 执行查询并输出结果
|
||||
$result = mysqli_query($conn, $sql);
|
||||
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;
|
||||
$where = VisitorLogListHelper::buildWhere($conn, $_GET);
|
||||
|
||||
if(!empty($_GET['visit_date'])) {
|
||||
$map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'";
|
||||
}
|
||||
|
||||
$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);
|
||||
$sql = VisitorLogListHelper::listSelectSql($where, $startPosition, $recordsPerPage, true);
|
||||
$result = $conn->query($sql);
|
||||
$jsonData = array();
|
||||
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
//echo "<p>{$row["columnName"]}</p>"; // 根据需要修改列名
|
||||
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; // 访问次数叠加
|
||||
if ($result) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$jsonData[] = VisitorLogListHelper::formatListRow($row, $countries, $reasons);
|
||||
}
|
||||
|
||||
// 更新访问次数
|
||||
$updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id'];
|
||||
mysqli_query($conn, $updateSql);
|
||||
$result->free();
|
||||
}
|
||||
|
||||
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);
|
||||
$conn->close();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
echo json_encode($jsonResult);exit;
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($jsonResult);
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* 二次风控判定解析(按需加载,供日志列表点击「判定详情」)
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/cong.php';
|
||||
require_once dirname(__DIR__) . '/lib/DbHelper.php';
|
||||
require_once dirname(__DIR__) . '/datatable/auth.php';
|
||||
|
||||
cloak_datatable_require_auth();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
if ($id <= 0) {
|
||||
echo json_encode(['code' => 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);
|
||||
Regular → Executable
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* 日志大字段按需加载(fp_hdata、api_* 等)
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/cong.php';
|
||||
require_once dirname(__DIR__) . '/lib/DbHelper.php';
|
||||
require_once dirname(__DIR__) . '/datatable/auth.php';
|
||||
|
||||
cloak_datatable_require_auth();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
$id = isset($_GET['id']) ? (int) $_GET['id'] : 0;
|
||||
$field = isset($_GET['field']) ? (string) $_GET['field'] : '';
|
||||
|
||||
$allowed = [
|
||||
'fp_hdata' => '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);
|
||||
+21
-106
@@ -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`";
|
||||
VisitorLogSchema::ensureIndexes($conn);
|
||||
|
||||
// 执行查询并输出结果
|
||||
$result = mysqli_query($conn, $sql);
|
||||
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;
|
||||
$where = VisitorLogListHelper::buildWhere($conn, $_GET);
|
||||
|
||||
if(!empty($_GET['visit_date'])) {
|
||||
$map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'";
|
||||
}
|
||||
|
||||
$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);
|
||||
$sql = VisitorLogListHelper::listSelectSql($where, $startPosition, $recordsPerPage, false);
|
||||
$result = $conn->query($sql);
|
||||
$jsonData = array();
|
||||
|
||||
while ($row = mysqli_fetch_assoc($result)) {
|
||||
//echo "<p>{$row["columnName"]}</p>"; // 根据需要修改列名
|
||||
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; // 访问次数叠加
|
||||
if ($result) {
|
||||
while ($row = $result->fetch_assoc()) {
|
||||
$jsonData[] = VisitorLogListHelper::formatListRow($row, $countries, $reasons);
|
||||
}
|
||||
|
||||
// 更新访问次数
|
||||
$updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id'];
|
||||
mysqli_query($conn, $updateSql);
|
||||
$result->free();
|
||||
}
|
||||
|
||||
$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);
|
||||
$conn->close();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
echo json_encode($jsonResult);exit;
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode($jsonResult);
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/cong.php';
|
||||
require_once dirname(__DIR__) . '/lib/bootstrap.php';
|
||||
require_once dirname(__DIR__) . '/lib/Cloak/VisitorSimulationRunner.php';
|
||||
session_start();
|
||||
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
|
||||
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
http_response_code(403);
|
||||
echo json_encode(['ok' => 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);
|
||||
+19
-167
@@ -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请求'
|
||||
'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 . "<br>" . $conn->error . "\n");
|
||||
fclose($handle);
|
||||
}
|
||||
$conn->close();
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
exit(json_encode($response));
|
||||
unset($result['ok'], $result['status'], $result['fingerprint_info'], $result['api_request'], $result['api_response']);
|
||||
echo json_encode($result);
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<?php
|
||||
|
||||
require('ip_check.php');
|
||||
|
||||
+13
-7
@@ -1,29 +1,29 @@
|
||||
-- Cloaka 安装程序自动执行的表结构(与 lib/InstallSchema.php 保持一致)
|
||||
-- 使用 CREATE TABLE IF NOT EXISTS,重复安装不会覆盖已有数据
|
||||
|
||||
CREATE TABLE `cloak_site_domains` (
|
||||
CREATE TABLE IF NOT EXISTS `cloak_site_domains` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`hostname` varchar(255) NOT NULL COMMENT '站点域名,不含协议与路径',
|
||||
`config_name` varchar(64) NOT NULL DEFAULT '' COMMENT '关联 check_config 配置名',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
`hostname` varchar(255) NOT NULL COMMENT '站点域名',
|
||||
`config_name` varchar(64) NOT NULL DEFAULT '' COMMENT '关联配置名',
|
||||
`cf_zone_id` varchar(64) DEFAULT NULL COMMENT 'Cloudflare Zone ID',
|
||||
`cf_nameservers` text DEFAULT NULL COMMENT 'Cloudflare NS JSON',
|
||||
`cf_status` varchar(32) NOT NULL DEFAULT 'local' COMMENT 'Cloudflare 状态',
|
||||
`cf_error` varchar(500) DEFAULT NULL COMMENT '最近错误',
|
||||
`cf_checked_at` datetime DEFAULT NULL COMMENT '上次检测时间',
|
||||
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_hostname` (`hostname`),
|
||||
KEY `idx_config_name` (`config_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `ip_groups` (
|
||||
CREATE TABLE IF NOT EXISTS `ip_groups` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(100) NOT NULL COMMENT '组名',
|
||||
`type` enum('black','white') NOT NULL COMMENT '类型:black为黑名单,white为白名单',
|
||||
PRIMARY KEY (`id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `ip_list` (
|
||||
CREATE TABLE IF NOT EXISTS `ip_list` (
|
||||
`id` int(11) NOT NULL AUTO_INCREMENT,
|
||||
`group_id` int(11) NOT NULL COMMENT '所属组ID',
|
||||
`ip_address` varchar(45) NOT NULL COMMENT 'IP地址',
|
||||
@@ -31,7 +31,7 @@ CREATE TABLE `ip_list` (
|
||||
KEY `idx_group_ip` (`group_id`,`ip_address`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
|
||||
|
||||
CREATE TABLE `visitor_logs` (
|
||||
CREATE TABLE IF NOT EXISTS `visitor_logs` (
|
||||
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
|
||||
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
|
||||
`visit_md5_code` char(32) NOT NULL DEFAULT 'loss' COMMENT '客户端标记',
|
||||
@@ -54,6 +54,12 @@ CREATE TABLE `visitor_logs` (
|
||||
`accept_language_raw` text DEFAULT NULL COMMENT 'Accept-Language 完整值',
|
||||
`judge_timing` text DEFAULT NULL COMMENT '判定用时 JSON',
|
||||
`fp_hdata` text DEFAULT NULL COMMENT '指纹 hdata 解码 JSON',
|
||||
`api_by_request` text DEFAULT NULL COMMENT 'byApi 请求参数 JSON',
|
||||
`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`),
|
||||
|
||||
+3
-1
@@ -1,7 +1,9 @@
|
||||
<?php
|
||||
require_once("cong.php");
|
||||
require_once __DIR__ . '/lib/CloakHttpHelpers.php';
|
||||
require_once __DIR__ . '/lib/Cloak/CloakSession.php';
|
||||
|
||||
session_start();
|
||||
CloakSession::start();
|
||||
// $begin_time = date("H:i:s");
|
||||
// include("ip_check_config.php");
|
||||
|
||||
|
||||
@@ -101,3 +101,15 @@
|
||||
2026-06-14 15:47:16 | 写入数据库 | false | index | async=1
|
||||
2026-06-15 16:45:08 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 | https://www.baidu.com/
|
||||
2026-06-15 16:45:08 | 写入数据库 | false | index | async=1
|
||||
2026-06-15 19:13:24 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 |
|
||||
2026-06-15 19:13:24 | 写入数据库 | false | index | async=1
|
||||
2026-06-15 20:30:01 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 |
|
||||
2026-06-15 20:30:01 | 写入数据库 | false | indBXSXX | async=1
|
||||
2026-06-15 20:30:45 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 |
|
||||
2026-06-15 20:30:45 | 写入数据库 | false | indBXSXX | async=1
|
||||
2026-06-15 20:31:04 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 |
|
||||
2026-06-15 20:31:04 | 写入数据库 | false | indBXSXX | async=1
|
||||
2026-06-15 20:31:20 | 来源判断 | 无法识别访客国家|false | index | 127.0.0.1 |
|
||||
2026-06-15 20:31:20 | 写入数据库 | false | indBXSXX | async=1
|
||||
2026-06-15 20:31:51 | 来源判断 | 无法识别访客国家|false | indBXSXX | 127.0.0.1 |
|
||||
2026-06-15 20:31:51 | 写入数据库 | false | indBXSXX | async=1
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* Session 与 f_check 跨域(CORS)辅助
|
||||
*/
|
||||
class CloakSession
|
||||
{
|
||||
public static function start()
|
||||
{
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
return;
|
||||
}
|
||||
|
||||
$domain = function_exists('cloak_fingerprint_cookie_domain')
|
||||
? cloak_fingerprint_cookie_domain()
|
||||
: '';
|
||||
|
||||
if ($domain !== '' && PHP_VERSION_ID >= 70300) {
|
||||
session_set_cookie_params([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => $domain,
|
||||
'secure' => function_exists('cloak_request_is_https') && cloak_request_is_https(),
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
} elseif ($domain !== '') {
|
||||
session_set_cookie_params(
|
||||
0,
|
||||
'/',
|
||||
$domain,
|
||||
function_exists('cloak_request_is_https') && cloak_request_is_https(),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
session_start();
|
||||
}
|
||||
}
|
||||
|
||||
class CloakFcheckCors
|
||||
{
|
||||
/**
|
||||
* OPTIONS 预检:在 session/bootstrap 之前调用。
|
||||
*/
|
||||
public static function handlePreflight()
|
||||
{
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'OPTIONS') {
|
||||
return;
|
||||
}
|
||||
|
||||
self::emitHeaders();
|
||||
http_response_code(204);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 响应头(含 AJAX 跨子域 withCredentials)
|
||||
*/
|
||||
public static function emitHeaders()
|
||||
{
|
||||
$origin = (string) ($_SERVER['HTTP_ORIGIN'] ?? '');
|
||||
if ($origin !== '' && self::isAllowedOrigin($origin)) {
|
||||
header('Access-Control-Allow-Origin: ' . $origin);
|
||||
header('Access-Control-Allow-Credentials: true');
|
||||
header('Vary: Origin');
|
||||
}
|
||||
|
||||
header('Access-Control-Allow-Methods: POST, OPTIONS');
|
||||
header('Access-Control-Allow-Headers: Content-Type');
|
||||
header('Access-Control-Max-Age: 86400');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $origin
|
||||
*/
|
||||
private static function isAllowedOrigin($origin)
|
||||
{
|
||||
$originHost = parse_url($origin, PHP_URL_HOST);
|
||||
if (!is_string($originHost) || $originHost === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$requestHost = (string) ($_SERVER['HTTP_HOST'] ?? '');
|
||||
if ($requestHost !== '' && strcasecmp($originHost, $requestHost) === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$suffix = self::registrableSuffix($requestHost);
|
||||
if ($suffix === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$originSuffix = self::registrableSuffix($originHost);
|
||||
if ($originSuffix === '' || strcasecmp($originSuffix, $suffix) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::hostMatchesSuffix($originHost, $suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
*/
|
||||
private static function registrableSuffix($host)
|
||||
{
|
||||
$host = strtolower(trim($host));
|
||||
if ($host === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (function_exists('cloak_fingerprint_cookie_domain')) {
|
||||
$cookieDomain = ltrim(cloak_fingerprint_cookie_domain(), '.');
|
||||
if ($cookieDomain !== '' && self::hostMatchesSuffix($host, $cookieDomain)) {
|
||||
return $cookieDomain;
|
||||
}
|
||||
}
|
||||
|
||||
$parts = explode('.', $host);
|
||||
if (count($parts) < 2) {
|
||||
return $host;
|
||||
}
|
||||
|
||||
return $parts[count($parts) - 2] . '.' . $parts[count($parts) - 1];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
* @param string $suffix
|
||||
*/
|
||||
private static function hostMatchesSuffix($host, $suffix)
|
||||
{
|
||||
$host = strtolower($host);
|
||||
$suffix = strtolower($suffix);
|
||||
|
||||
return $host === $suffix || substr($host, -strlen('.' . $suffix)) === '.' . $suffix;
|
||||
}
|
||||
}
|
||||
Regular → Executable
+21
-4
@@ -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
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,288 @@
|
||||
<?php
|
||||
/**
|
||||
* 二次风控(byApiRisk)处理,供 f_check.php 与模拟测试复用
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/DbHelper.php';
|
||||
require_once __DIR__ . '/FpUrlHelper.php';
|
||||
require_once __DIR__ . '/VisitorApiAudit.php';
|
||||
require_once __DIR__ . '/RiskSecondaryGate.php';
|
||||
require_once __DIR__ . '/RiskSecondaryCache.php';
|
||||
require_once __DIR__ . '/VisitorLogSchema.php';
|
||||
require_once __DIR__ . '/ClientIpResolver.php';
|
||||
|
||||
class FCheckHandler
|
||||
{
|
||||
/**
|
||||
* @param string $hdata base64(JSON)
|
||||
* @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
|
||||
*/
|
||||
public static function processFromHdata($hdata)
|
||||
{
|
||||
$decodedString = base64_decode((string) $hdata, true);
|
||||
$fingerprint_info = is_string($decodedString) ? json_decode($decodedString, true) : null;
|
||||
if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 400,
|
||||
'message' => '无效的指纹数据',
|
||||
];
|
||||
}
|
||||
return self::process($fingerprint_info, (string) $hdata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $fingerprint_info
|
||||
* @param string|null $hdata base64 payload sent to API;为空时自动生成
|
||||
* @return array{ok:bool,status?:int,result?:bool,reason?:string,link?:string,message?:string,fingerprint_info?:array,api_request?:array,api_response?:array,cached?:bool}
|
||||
*/
|
||||
public static function process(array $fingerprint_info, $hdata = null)
|
||||
{
|
||||
$fp_host = parse_url($fingerprint_info['domain'], PHP_URL_HOST);
|
||||
if (empty($fp_host)) {
|
||||
$fp_host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
}
|
||||
if (!class_exists('ConfigLoader', false)) {
|
||||
require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
|
||||
}
|
||||
$config_name = ConfigLoader::loadByHost($fp_host);
|
||||
$log_id = (int) ($fingerprint_info['log_id'] ?? 0);
|
||||
|
||||
if (!RiskSecondaryGate::isEnabled()) {
|
||||
$_SESSION['visit_to_3'] = 3;
|
||||
$_SESSION['check_result'] = 'true';
|
||||
|
||||
$resolved = FpUrlHelper::resolve($config_name, true);
|
||||
$fp_url = $resolved['url'];
|
||||
$response = [
|
||||
'reason' => '',
|
||||
'result' => true,
|
||||
'link' => $fp_url !== '' ? $fp_url : FpUrlHelper::fallbackUrl(),
|
||||
];
|
||||
|
||||
if ($log_id > 0) {
|
||||
self::updateLog($log_id, [
|
||||
'result' => 'true',
|
||||
'reason' => '二次风控已关闭',
|
||||
'fp_url' => $response['link'],
|
||||
], $fingerprint_info, null, null);
|
||||
}
|
||||
|
||||
return array_merge(['ok' => true, 'status' => 200, 'fingerprint_info' => $fingerprint_info], $response);
|
||||
}
|
||||
|
||||
$clientIp = ClientIpResolver::resolve();
|
||||
$cached = RiskSecondaryCache::get($clientIp);
|
||||
if ($cached !== null) {
|
||||
return self::finishFromCache($cached, $config_name, $log_id, $fingerprint_info);
|
||||
}
|
||||
|
||||
if ($hdata === null || $hdata === '') {
|
||||
$json = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
$hdata = base64_encode($json !== false ? $json : '{}');
|
||||
}
|
||||
|
||||
$jsonData = [
|
||||
'id' => COSTM_IP_SCORE,
|
||||
'hdata' => $hdata,
|
||||
'referer' => $fingerprint_info['referer'] ?? '',
|
||||
'domain' => $fingerprint_info['domain'],
|
||||
'ip' => $fingerprint_info['ip'] ?? '',
|
||||
'risk_enhanced' => defined('CLOAK_RISK_ENHANCED') && strtoupper(CLOAK_RISK_ENHANCED) === 'ON' ? 1 : 0,
|
||||
'risk_number' => CLOAK_RISK_NUMBER,
|
||||
];
|
||||
|
||||
$ch = curl_init('www.tiktokba.com/cloak/byApiRisk');
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_setopt($ch, CURLOPT_USERAGENT, get_SERVER_value('HTTP_USER_AGENT'));
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
curl_setopt($ch, CURLOPT_ENCODING, '');
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-type: application/x-www-form-urlencoded',
|
||||
'escloak-key: ' . CHECK_KEY,
|
||||
]);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($jsonData));
|
||||
if (function_exists('forward_response_cookies')) {
|
||||
curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'forward_response_cookies');
|
||||
}
|
||||
if (!empty($_COOKIE) && function_exists('encode_visitor_cookies')) {
|
||||
curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies());
|
||||
}
|
||||
|
||||
$returnRaw = curl_exec($ch);
|
||||
if ($returnRaw === false) {
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 502,
|
||||
'message' => 'Curl error: ' . $err,
|
||||
];
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
$return = json_decode($returnRaw, true);
|
||||
if (!is_array($return)) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'status' => 502,
|
||||
'message' => '风控 API 返回无效',
|
||||
];
|
||||
}
|
||||
|
||||
return self::finishFromApi($return, $config_name, $log_id, $fingerprint_info, $jsonData, $clientIp);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $return byApiRisk 原始响应
|
||||
* @param array $jsonData 请求体
|
||||
*/
|
||||
private static function finishFromApi(array $return, $config_name, $log_id, array $fingerprint_info, array $jsonData, $clientIp)
|
||||
{
|
||||
$_SESSION['visit_to_3'] = 3;
|
||||
$response = [];
|
||||
$response['reason'] = $return['reason'] ?? '';
|
||||
$response['result'] = !empty($return['result']);
|
||||
|
||||
if ($response['result']) {
|
||||
$_SESSION['check_result'] = 'true';
|
||||
$resolved = FpUrlHelper::resolve($config_name, true);
|
||||
$response['link'] = $resolved['url'];
|
||||
} else {
|
||||
$_SESSION['check_result'] = 'false';
|
||||
if (CLOAK_REDIRECT_METHOD == 'curl') {
|
||||
$response['link'] = '';
|
||||
} else {
|
||||
$response['link'] = DB_ZP;
|
||||
}
|
||||
}
|
||||
|
||||
$update_log = self::buildUpdateLog($response);
|
||||
self::persistCache($update_log, $clientIp);
|
||||
|
||||
if ($log_id > 0) {
|
||||
self::updateLog($log_id, $update_log, $fingerprint_info, $jsonData, $return);
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'ok' => true,
|
||||
'status' => 200,
|
||||
'fingerprint_info' => $fingerprint_info,
|
||||
'api_request' => $jsonData,
|
||||
'api_response' => $return,
|
||||
'cached' => false,
|
||||
], $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $cached RiskSecondaryCache::get()
|
||||
*/
|
||||
private static function finishFromCache(array $cached, $config_name, $log_id, array $fingerprint_info)
|
||||
{
|
||||
RiskSecondaryCache::applyToSession($cached);
|
||||
$response = RiskSecondaryCache::toApiResponse($cached);
|
||||
|
||||
$update_log = [
|
||||
'result' => $response['result'] ? 'true' : 'false',
|
||||
'reason' => $response['reason'],
|
||||
'fp_url' => trim((string) ($response['link'] ?? '')),
|
||||
];
|
||||
if ($update_log['fp_url'] === '') {
|
||||
$update_log['fp_url'] = $update_log['result'] === 'true'
|
||||
? FpUrlHelper::fallbackUrl()
|
||||
: (defined('DB_ZP') ? (string) DB_ZP : '');
|
||||
}
|
||||
|
||||
if ($log_id > 0) {
|
||||
$cacheMarker = [
|
||||
'cached' => true,
|
||||
'result' => $update_log['result'],
|
||||
'reason' => $cached['reason'] ?? '',
|
||||
'api_reason' => $cached['reason'] ?? '',
|
||||
'cached_at' => $cached['cached_at'] ?? null,
|
||||
];
|
||||
self::updateLog($log_id, $update_log, $fingerprint_info, null, $cacheMarker);
|
||||
}
|
||||
|
||||
return array_merge([
|
||||
'ok' => true,
|
||||
'status' => 200,
|
||||
'fingerprint_info' => $fingerprint_info,
|
||||
'cached' => true,
|
||||
], $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{result:bool,reason?:string,link?:string} $response
|
||||
* @return array{result:string,reason:string,fp_url:string}
|
||||
*/
|
||||
private static function buildUpdateLog(array $response)
|
||||
{
|
||||
$update_log = [];
|
||||
$update_log['result'] = !empty($response['result']) ? 'true' : 'false';
|
||||
$update_log['reason'] = $response['reason'] ?? '';
|
||||
$update_log['fp_url'] = trim((string) ($response['link'] ?? ''));
|
||||
if ($update_log['fp_url'] === '') {
|
||||
$update_log['fp_url'] = $update_log['result'] === 'true'
|
||||
? FpUrlHelper::fallbackUrl()
|
||||
: (defined('DB_ZP') ? (string) DB_ZP : '');
|
||||
}
|
||||
|
||||
return $update_log;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{result:string,reason:string,fp_url:string} $update_log
|
||||
* @param string $clientIp
|
||||
*/
|
||||
private static function persistCache(array $update_log, $clientIp)
|
||||
{
|
||||
RiskSecondaryCache::store([
|
||||
'ip' => $clientIp,
|
||||
'result' => $update_log['result'],
|
||||
'reason' => $update_log['reason'],
|
||||
'fp_url' => $update_log['fp_url'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $log_id
|
||||
* @param array $update_log result, reason, fp_url
|
||||
* @param array $fingerprint_info
|
||||
* @param array|null $apiRequest
|
||||
* @param array|null $apiResponse
|
||||
*/
|
||||
public static function updateLog($log_id, array $update_log, array $fingerprint_info, $apiRequest, $apiResponse)
|
||||
{
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if (!$conn || $conn->connect_error) {
|
||||
return false;
|
||||
}
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
|
||||
$resultEsc = cloak_db_escape($conn, $update_log['result'] ?? '');
|
||||
$reasonEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['reason'] ?? '')));
|
||||
$fpUrlEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['fp_url'] ?? '')));
|
||||
$fpHdataJson = json_encode($fingerprint_info, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($fpHdataJson === false) {
|
||||
$fpHdataJson = '';
|
||||
}
|
||||
$fpHdataEsc = cloak_db_escape($conn, $fpHdataJson);
|
||||
$apiRiskReqEsc = cloak_db_escape($conn, $apiRequest !== null ? VisitorApiAudit::encode($apiRequest) : '');
|
||||
$apiRiskRespEsc = cloak_db_escape($conn, $apiResponse !== null ? VisitorApiAudit::encode($apiResponse) : '');
|
||||
$sql = "UPDATE `visitor_logs` SET `result`='{$resultEsc}',`reason`='{$reasonEsc}',`fp_url`='{$fpUrlEsc}',`fp_hdata`='{$fpHdataEsc}',`api_risk_request`='{$apiRiskReqEsc}',`api_risk_response`='{$apiRiskRespEsc}' WHERE `id`='" . (int) $log_id . "'";
|
||||
|
||||
$ok = $conn->query($sql) === true;
|
||||
if (!$ok) {
|
||||
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
if ($handle) {
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | FCheckHandler update: ' . $conn->error . "\n");
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
$conn->close();
|
||||
return $ok;
|
||||
}
|
||||
}
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -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
|
||||
*/
|
||||
|
||||
Regular → Executable
+4
-1
@@ -35,7 +35,10 @@ if (!CountryAllowlist::isAllowed($countryCode, $allowlist)) {
|
||||
if ($countryCode === null || $countryCode === '') {
|
||||
$reason = '无法识别访客国家';
|
||||
} else {
|
||||
$reason = '国家不符:' . $countryCode;
|
||||
$reason = '国家不符:访客 ' . $countryCode;
|
||||
if ($allowlist !== []) {
|
||||
$reason .= ',允许 ' . implode(',', $allowlist);
|
||||
}
|
||||
}
|
||||
if (!empty($GLOBALS['__cloak_debug_on'])) {
|
||||
cloak_dbg_step(__LINE__, 'GeoIP国家判定', 'fail', $reason, [
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<?php
|
||||
//判断临时链接参数(Guard 之后、API/指纹 之前)
|
||||
if (!empty($GLOBALS['__cloak_simulation_on'])) {
|
||||
return;
|
||||
}
|
||||
if(empty($_SESSION["check_result"]) && !$whiteParamsMatched && SHOW_SITE_MODE_SWITCH == 'ip_check' && CLOAK_URL_ARGS_TIMEOUT > 0) {
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'临时链接参数', 'info', 'CLOAK_URL_ARGS_TIMEOUT=' . CLOAK_URL_ARGS_TIMEOUT . ' 分钟,开始检查');
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
cloak_dbg_step(__LINE__, 'JS 指纹检测', 'skip', 'DEBUG 跳过指纹 JS 跳转,直接进入 API 判定');
|
||||
CloakDebugPage::setFlag('skipped_fingerprint', true);
|
||||
$v_info->check_result = 'true';
|
||||
} elseif (!empty($GLOBALS['__cloak_simulation_on'])) {
|
||||
$v_info->check_result = 'true';
|
||||
} else {
|
||||
FingerprintRedirectRenderer::renderAndExit($config_name);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/ClientIpResolver.php';
|
||||
require_once __DIR__ . '/FpUrlHelper.php';
|
||||
|
||||
/**
|
||||
* 二次风控(byApiRisk)Session 缓存:同 Session 内复用判定结果,IP 变化时失效。
|
||||
*/
|
||||
class RiskSecondaryCache
|
||||
{
|
||||
const SESSION_KEY = 'cloak_risk_cache';
|
||||
|
||||
/**
|
||||
* @param array{result:string,reason?:string,fp_url?:string,ip?:string} $payload
|
||||
*/
|
||||
public static function store(array $payload)
|
||||
{
|
||||
$ip = ClientIpResolver::normalizeIp($payload['ip'] ?? ClientIpResolver::resolve());
|
||||
if ($ip === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$_SESSION[self::SESSION_KEY] = [
|
||||
'ip' => $ip,
|
||||
'result' => ($payload['result'] ?? '') === 'true' ? 'true' : 'false',
|
||||
'reason' => (string) ($payload['reason'] ?? ''),
|
||||
'fp_url' => (string) ($payload['fp_url'] ?? ''),
|
||||
'risk_number' => defined('CLOAK_RISK_NUMBER') ? (int) CLOAK_RISK_NUMBER : 0,
|
||||
'campaign_id' => defined('COSTM_IP_SCORE') ? (string) COSTM_IP_SCORE : '',
|
||||
'cached_at' => time(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $currentIp 为空时使用 ClientIpResolver::resolve()
|
||||
* @return array|null
|
||||
*/
|
||||
public static function get($currentIp = null)
|
||||
{
|
||||
if (empty($_SESSION[self::SESSION_KEY]) || !is_array($_SESSION[self::SESSION_KEY])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cached = $_SESSION[self::SESSION_KEY];
|
||||
$ip = ClientIpResolver::normalizeIp($currentIp !== null && $currentIp !== '' ? $currentIp : ClientIpResolver::resolve());
|
||||
$cachedIp = ClientIpResolver::normalizeIp($cached['ip'] ?? '');
|
||||
|
||||
$invalidate = false;
|
||||
if ($ip === null || $cachedIp === null || $ip !== $cachedIp) {
|
||||
$invalidate = true;
|
||||
} elseif (defined('CLOAK_RISK_NUMBER') && (int) ($cached['risk_number'] ?? -1) !== (int) CLOAK_RISK_NUMBER) {
|
||||
$invalidate = true;
|
||||
} elseif (defined('COSTM_IP_SCORE') && (string) ($cached['campaign_id'] ?? '') !== (string) COSTM_IP_SCORE) {
|
||||
$invalidate = true;
|
||||
}
|
||||
|
||||
if ($invalidate) {
|
||||
self::clear();
|
||||
$_SESSION['visit_to_3'] = '1';
|
||||
return null;
|
||||
}
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
public static function clear()
|
||||
{
|
||||
unset($_SESSION[self::SESSION_KEY]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将缓存同步到 visit_to_3 / check_result,供 RouteResolver 等复用。
|
||||
*
|
||||
* @param array $cached
|
||||
*/
|
||||
public static function applyToSession(array $cached)
|
||||
{
|
||||
$_SESSION['visit_to_3'] = 3;
|
||||
$_SESSION['check_result'] = ($cached['result'] ?? '') === 'true' ? 'true' : 'false';
|
||||
}
|
||||
|
||||
public static function hitReason()
|
||||
{
|
||||
return '二次风控缓存命中';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $cached
|
||||
* @return array{result:bool,reason:string,link:string}
|
||||
*/
|
||||
public static function toApiResponse(array $cached)
|
||||
{
|
||||
$passed = ($cached['result'] ?? '') === 'true';
|
||||
$link = (string) ($cached['fp_url'] ?? '');
|
||||
|
||||
if ($passed && $link === '') {
|
||||
$link = FpUrlHelper::fallbackUrl();
|
||||
}
|
||||
if (!$passed && $link === '' && defined('DB_ZP')) {
|
||||
$link = (string) DB_ZP;
|
||||
}
|
||||
|
||||
return [
|
||||
'result' => $passed,
|
||||
'reason' => self::hitReason(),
|
||||
'link' => $link,
|
||||
];
|
||||
}
|
||||
}
|
||||
Regular → Executable
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/FpUrlHelper.php';
|
||||
require_once __DIR__ . '/RiskSecondaryGate.php';
|
||||
require_once __DIR__ . '/RiskSecondaryCache.php';
|
||||
require_once __DIR__ . '/ClientIpResolver.php';
|
||||
|
||||
/**
|
||||
* 模拟测试路由:写日志,不输出 HTML / 302
|
||||
*/
|
||||
class SimulationRouteResolver
|
||||
{
|
||||
/**
|
||||
* @param array $ctx 需含 v_info, logs, config_name, reason, model
|
||||
* @return array{branch:string,log_id:?int,logs:array,needs_secondary:bool}
|
||||
*/
|
||||
public static function dispatch(array $ctx)
|
||||
{
|
||||
extract($ctx, EXTR_SKIP);
|
||||
|
||||
if (SHOW_SITE_MODE_SWITCH == 'zp') {
|
||||
return self::safeBranch($logs, $v_info);
|
||||
}
|
||||
|
||||
$showFp = (SHOW_SITE_MODE_SWITCH == 'ip_check' && $_SESSION['check_result'] != 'false')
|
||||
|| SHOW_SITE_MODE_SWITCH == 'fp';
|
||||
|
||||
if ($showFp) {
|
||||
return self::fpOrRiskBranch($logs, $v_info, $config_name);
|
||||
}
|
||||
|
||||
return self::safeBranch($logs, $v_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs
|
||||
*/
|
||||
private static function safeBranch(array $logs, $v_info)
|
||||
{
|
||||
if (SHOW_SITE_MODE_SWITCH == 'zp') {
|
||||
$logs['result'] = 'false';
|
||||
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', '正品模式');
|
||||
}
|
||||
|
||||
$logs['fp_url'] = DB_ZP;
|
||||
$log_id = write_log_db($logs);
|
||||
|
||||
return [
|
||||
'branch' => 'safe_page',
|
||||
'log_id' => $log_id,
|
||||
'logs' => $logs,
|
||||
'needs_secondary' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs
|
||||
*/
|
||||
private static function fpOrRiskBranch(array $logs, $v_info, $config_name)
|
||||
{
|
||||
if (AUTO_BLACK == 'ON') {
|
||||
$times = howmanytimesbyip($v_info->v_ip);
|
||||
if ($times + 1 >= AUTO_BLACK_TIMES) {
|
||||
write_black_list($v_info->v_ip);
|
||||
}
|
||||
}
|
||||
if (!isset($_SESSION['visit_to_3'])) {
|
||||
$_SESSION['visit_to_3'] = '1';
|
||||
}
|
||||
|
||||
if (RiskSecondaryGate::isEnabled()) {
|
||||
$cached = RiskSecondaryCache::get(ClientIpResolver::resolve());
|
||||
if ($cached !== null) {
|
||||
return self::fpFromRiskCache($logs, $v_info, $config_name, $cached);
|
||||
}
|
||||
}
|
||||
|
||||
if (($_SESSION['visit_to_3'] ?? '1') != '3' && RiskSecondaryGate::isEnabled()) {
|
||||
$logs['result'] = 'wait';
|
||||
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', '二次风控检测中');
|
||||
$logs['fp_url'] = '';
|
||||
$log_id = write_log_db($logs);
|
||||
|
||||
return [
|
||||
'branch' => 'secondary_risk',
|
||||
'log_id' => $log_id,
|
||||
'logs' => $logs,
|
||||
'needs_secondary' => true,
|
||||
];
|
||||
}
|
||||
|
||||
$logs['result'] = 'true';
|
||||
$logs['reason'] = $logs['reason'] ?? '';
|
||||
$resolved = FpUrlHelper::resolve($config_name, true);
|
||||
$logs['fp_url'] = $resolved['url'] !== '' ? $resolved['url'] : FpUrlHelper::fallbackUrl();
|
||||
$log_id = write_log_db($logs);
|
||||
|
||||
return [
|
||||
'branch' => 'fp_page',
|
||||
'log_id' => $log_id,
|
||||
'logs' => $logs,
|
||||
'needs_secondary' => false,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs
|
||||
* @param array $cached
|
||||
*/
|
||||
private static function fpFromRiskCache(array $logs, $v_info, $config_name, array $cached)
|
||||
{
|
||||
RiskSecondaryCache::applyToSession($cached);
|
||||
$apiResponse = RiskSecondaryCache::toApiResponse($cached);
|
||||
|
||||
$logs['result'] = $apiResponse['result'] ? 'true' : 'false';
|
||||
$logs['reason'] = self::mergeReason($logs['reason'] ?? '', $apiResponse['reason']);
|
||||
$logs['fp_url'] = trim((string) ($apiResponse['link'] ?? ''));
|
||||
if ($logs['fp_url'] === '') {
|
||||
$logs['fp_url'] = $logs['result'] === 'true'
|
||||
? FpUrlHelper::fallbackUrl()
|
||||
: (defined('DB_ZP') ? (string) DB_ZP : '');
|
||||
}
|
||||
|
||||
if ($logs['result'] === 'false') {
|
||||
return self::safeBranch($logs, $v_info);
|
||||
}
|
||||
|
||||
$log_id = write_log_db($logs);
|
||||
|
||||
return [
|
||||
'branch' => 'fp_page_cache',
|
||||
'log_id' => $log_id,
|
||||
'logs' => $logs,
|
||||
'needs_secondary' => false,
|
||||
];
|
||||
}
|
||||
|
||||
private static function mergeReason($existing, $append)
|
||||
{
|
||||
$existing = trim((string) $existing);
|
||||
$append = trim((string) $append);
|
||||
if ($existing === '') {
|
||||
return $append;
|
||||
}
|
||||
if ($append === '' || strpos($existing, $append) !== false) {
|
||||
return $existing;
|
||||
}
|
||||
return $existing . ';' . $append;
|
||||
}
|
||||
}
|
||||
Regular → Executable
Regular → Executable
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* 访客日志列表查询与行格式化(datalist / sdatalist 共用,避免重复重逻辑)
|
||||
*/
|
||||
class VisitorLogListHelper
|
||||
{
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param array $get
|
||||
*/
|
||||
public static function buildWhere($conn, array $get)
|
||||
{
|
||||
$where = '1';
|
||||
$where .= cloak_visitor_log_apply_result_filter($conn, $get['result'] ?? '');
|
||||
$map = [];
|
||||
|
||||
if (!empty($get['visit_date'])) {
|
||||
$day = trim((string) $get['visit_date']);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $day)) {
|
||||
$dayEsc = cloak_db_escape($conn, $day);
|
||||
$map[] = "`visit_date` >= '{$dayEsc} 00:00:00' AND `visit_date` < DATE_ADD('{$dayEsc} 00:00:00', INTERVAL 1 DAY)";
|
||||
}
|
||||
}
|
||||
if (!empty($get['country'])) {
|
||||
$map[] = "country LIKE '" . cloak_db_escape($conn, $get['country']) . "'";
|
||||
}
|
||||
if (!empty($get['IP'])) {
|
||||
$map[] = "IP LIKE '" . cloak_db_escape($conn, $get['IP']) . "'";
|
||||
}
|
||||
if (!empty($get['domain'])) {
|
||||
$map[] = "domain LIKE '%" . cloak_db_escape($conn, $get['domain']) . "%'";
|
||||
}
|
||||
if (!empty($get['spage'])) {
|
||||
$map[] = "page LIKE '%" . cloak_db_escape($conn, $get['spage']) . "%'";
|
||||
}
|
||||
if ($map !== []) {
|
||||
$where .= ' AND ' . implode(' AND ', $map);
|
||||
}
|
||||
|
||||
return $where;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表轻量 SELECT(不拉取 fp_hdata / api_* 等大 TEXT)
|
||||
*
|
||||
* @param bool $withSimulation
|
||||
*/
|
||||
public static function listSelectSql($where, $startPosition, $recordsPerPage, $withSimulation = true)
|
||||
{
|
||||
$cols = '`id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `device`, `judge_timing`, '
|
||||
. '(CASE WHEN `fp_hdata` IS NOT NULL AND `fp_hdata` <> \'\' THEN 1 ELSE 0 END) AS `has_fp_hdata`, '
|
||||
. '(CASE WHEN `api_by_request` IS NOT NULL AND `api_by_request` <> \'\' THEN 1 ELSE 0 END) AS `has_api_by_request`, '
|
||||
. '(CASE WHEN `api_by_response` IS NOT NULL AND `api_by_response` <> \'\' THEN 1 ELSE 0 END) AS `has_api_by_response`, '
|
||||
. '(CASE WHEN `api_risk_request` IS NOT NULL AND `api_risk_request` <> \'\' THEN 1 ELSE 0 END) AS `has_api_risk_request`, '
|
||||
. '(CASE WHEN `api_risk_response` IS NOT NULL AND `api_risk_response` <> \'\' THEN 1 ELSE 0 END) AS `has_api_risk_response`, '
|
||||
. '`add_time`';
|
||||
if ($withSimulation) {
|
||||
$cols .= ', `is_simulation`, `sim_source_log_id`';
|
||||
}
|
||||
|
||||
$startPosition = (int) $startPosition;
|
||||
$recordsPerPage = (int) $recordsPerPage;
|
||||
|
||||
return "SELECT {$cols} FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @param array $countries
|
||||
* @param array $reasons
|
||||
*/
|
||||
public static function formatListRow(array $row, array $countries, array $reasons)
|
||||
{
|
||||
if (!empty($row['country'])) {
|
||||
$row['country'] = $countries[$row['country']] ?? '无';
|
||||
} else {
|
||||
$row['country'] = '无';
|
||||
}
|
||||
if (isset($reasons[$row['reason']])) {
|
||||
$row['reason'] = $reasons[$row['reason']];
|
||||
}
|
||||
if (($row['result'] ?? '') === 'wait') {
|
||||
$row['result'] = '检测中';
|
||||
}
|
||||
|
||||
$row['judge_timing_text'] = cloak_visitor_log_format_timing($row['judge_timing'] ?? '');
|
||||
|
||||
if (!empty($row['has_fp_hdata'])) {
|
||||
$row['fp_hdata_text'] = '有指纹数据';
|
||||
} else {
|
||||
$row['fp_hdata_text'] = '';
|
||||
}
|
||||
|
||||
$row['api_by_request_text'] = self::auditPlaceholder($row, 'has_api_by_request');
|
||||
$row['api_by_response_text'] = self::auditPlaceholder($row, 'has_api_by_response');
|
||||
$row['api_risk_request_text'] = self::auditPlaceholder($row, 'has_api_risk_request');
|
||||
$row['api_risk_response_text'] = self::auditPlaceholder($row, 'has_api_risk_response');
|
||||
|
||||
unset(
|
||||
$row['has_fp_hdata'],
|
||||
$row['has_api_by_request'],
|
||||
$row['has_api_by_response'],
|
||||
$row['has_api_risk_request'],
|
||||
$row['has_api_risk_response']
|
||||
);
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @param string $flagKey
|
||||
*/
|
||||
private static function auditPlaceholder(array $row, $flagKey)
|
||||
{
|
||||
return !empty($row[$flagKey]) ? '__LAZY__' : '-';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
*/
|
||||
public static function countWhere($conn, $where)
|
||||
{
|
||||
$sql = "SELECT COUNT(*) AS `total` FROM `visitor_logs` WHERE {$where}";
|
||||
$res = $conn->query($sql);
|
||||
if (!$res) {
|
||||
return 0;
|
||||
}
|
||||
$row = $res->fetch_assoc();
|
||||
$res->free();
|
||||
|
||||
return (int) ($row['total'] ?? 0);
|
||||
}
|
||||
}
|
||||
Regular → Executable
@@ -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)) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* 访客日志「第几次访问」:写入时计算,避免列表接口 N+1 查询。
|
||||
*/
|
||||
class VisitorLogVtimes
|
||||
{
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $ip
|
||||
* @param string $domain
|
||||
*/
|
||||
public static function resolveForInsert($conn, $ip, $domain)
|
||||
{
|
||||
$ipEsc = cloak_db_escape($conn, $ip);
|
||||
$domainEsc = cloak_db_escape($conn, $domain);
|
||||
$sql = "SELECT COUNT(*) AS `c` FROM `visitor_logs` WHERE `IP`='{$ipEsc}' AND `domain`='{$domainEsc}'";
|
||||
$count = 0;
|
||||
if ($res = $conn->query($sql)) {
|
||||
$row = $res->fetch_assoc();
|
||||
$count = (int) ($row['c'] ?? 0);
|
||||
$res->free();
|
||||
}
|
||||
return $count + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs cloak_write_log_db 结构
|
||||
*/
|
||||
public static function mergeIntoLogs(array $logs, $conn)
|
||||
{
|
||||
if (!empty($logs['vtimes']) && (int) $logs['vtimes'] > 0) {
|
||||
return $logs;
|
||||
}
|
||||
$ip = (string) ($logs['customers_ip'] ?? '');
|
||||
$domain = (string) ($logs['site_name'] ?? '');
|
||||
if ($ip !== '' && $domain !== '') {
|
||||
$logs['vtimes'] = self::resolveForInsert($conn, $ip, $domain);
|
||||
}
|
||||
return $logs;
|
||||
}
|
||||
}
|
||||
Regular → Executable
+32
-4
@@ -5,6 +5,7 @@
|
||||
require_once dirname(__DIR__) . '/DbHelper.php';
|
||||
require_once __DIR__ . '/VisitorLogSchema.php';
|
||||
require_once __DIR__ . '/VisitorLogQueue.php';
|
||||
require_once __DIR__ . '/VisitorLogVtimes.php';
|
||||
|
||||
class VisitorLogWorker
|
||||
{
|
||||
@@ -65,12 +66,14 @@ class VisitorLogWorker
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
VisitorLogSchema::ensureIndexes($conn);
|
||||
|
||||
$logs = VisitorLogVtimes::mergeIntoLogs($logs, $conn);
|
||||
$data = self::escapeLogs($conn, $logs);
|
||||
$d = static function ($key) use ($data, $conn) {
|
||||
return array_key_exists($key, $data) ? $data[$key] : cloak_db_escape($conn, '');
|
||||
};
|
||||
list($simCols, $simVals) = self::simulationInsertFragment($data);
|
||||
|
||||
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`) VALUES ('"
|
||||
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `fp_url`, `device`, `api_by_request`, `api_by_response`, `api_risk_request`, `api_risk_response`{$simCols}) VALUES ('"
|
||||
. $d('campagin_id') . "','"
|
||||
. $d('visit_md5_code') . "','"
|
||||
. $d('site_name') . "','"
|
||||
@@ -80,6 +83,7 @@ class VisitorLogWorker
|
||||
. $d('result') . "','"
|
||||
. $d('reason') . "','"
|
||||
. $d('v_referer') . "','"
|
||||
. (int) ($data['vtimes'] ?? 1) . "','"
|
||||
. $d('Client') . "','"
|
||||
. $d('v_Browser') . "','"
|
||||
. $d('v_PageURL') . "','"
|
||||
@@ -93,7 +97,8 @@ class VisitorLogWorker
|
||||
. $d('api_by_request') . "','"
|
||||
. $d('api_by_response') . "','"
|
||||
. $d('api_risk_request') . "','"
|
||||
. $d('api_risk_response') . "')";
|
||||
. $d('api_risk_response') . "'"
|
||||
. $simVals . ')';
|
||||
|
||||
if ($conn->query($sql) !== true) {
|
||||
VisitorLogQueue::logError('insert_full: ' . $conn->error . ' | ' . $sql);
|
||||
@@ -120,12 +125,14 @@ class VisitorLogWorker
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
VisitorLogSchema::ensureIndexes($conn);
|
||||
|
||||
$logs = VisitorLogVtimes::mergeIntoLogs($logs, $conn);
|
||||
$data = self::escapeLogs($conn, $logs);
|
||||
$d = static function ($key) use ($data, $conn) {
|
||||
return array_key_exists($key, $data) ? $data[$key] : cloak_db_escape($conn, '');
|
||||
};
|
||||
list($simCols, $simVals) = self::simulationInsertFragment($data);
|
||||
|
||||
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `client`, `browser`, `language`, `fp_url`, `device`) VALUES ('"
|
||||
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `vtimes`, `client`, `browser`, `language`, `fp_url`, `device`{$simCols}) VALUES ('"
|
||||
. $d('campagin_id') . "','"
|
||||
. $d('visit_md5_code') . "','"
|
||||
. $d('site_name') . "','"
|
||||
@@ -134,11 +141,13 @@ class VisitorLogWorker
|
||||
. $d('country') . "','"
|
||||
. $d('result') . "','"
|
||||
. $d('reason') . "','"
|
||||
. (int) ($data['vtimes'] ?? 1) . "','"
|
||||
. $d('Client') . "','"
|
||||
. $d('v_Browser') . "','"
|
||||
. $d('accept_language') . "','"
|
||||
. $d('fp_url') . "','"
|
||||
. $d('device') . "')";
|
||||
. $d('device') . "'"
|
||||
. $simVals . ')';
|
||||
|
||||
if ($conn->query($sql) !== true) {
|
||||
VisitorLogQueue::logError('insertMinimal: ' . $conn->error);
|
||||
@@ -216,6 +225,25 @@ class VisitorLogWorker
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $data escaped log fields
|
||||
* @return array{0:string,1:string}
|
||||
*/
|
||||
private static function simulationInsertFragment(array $data)
|
||||
{
|
||||
if (empty($data['is_simulation']) && !array_key_exists('sim_source_log_id', $data)) {
|
||||
return ['', ''];
|
||||
}
|
||||
$cols = ', `is_simulation`, `sim_source_log_id`';
|
||||
$isSim = (int) ($data['is_simulation'] ?? 0);
|
||||
if (!empty($data['sim_source_log_id'])) {
|
||||
$vals = ", '{$isSim}', '" . (int) $data['sim_source_log_id'] . "'";
|
||||
} else {
|
||||
$vals = ", '{$isSim}', NULL";
|
||||
}
|
||||
return [$cols, $vals];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mysqli|null
|
||||
*/
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,591 @@
|
||||
<?php
|
||||
/**
|
||||
* 模拟测试访问:重建请求环境并跑完整判定链(含二次风控)
|
||||
*/
|
||||
require_once dirname(__DIR__, 2) . '/config/ConfigLoader.php';
|
||||
require_once __DIR__ . '/SimulationRouteResolver.php';
|
||||
require_once __DIR__ . '/FCheckHandler.php';
|
||||
require_once __DIR__ . '/DomainRepository.php';
|
||||
|
||||
class VisitorSimulationRunner
|
||||
{
|
||||
const REASON_PREFIX = '[模拟测试]';
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array
|
||||
*/
|
||||
public static function runManual(array $input)
|
||||
{
|
||||
$context = self::normalizeManualInput($input);
|
||||
if (!empty($context['error'])) {
|
||||
return ['ok' => false, 'message' => $context['error']];
|
||||
}
|
||||
return self::execute($context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @return array
|
||||
*/
|
||||
public static function replayFromLog($logId)
|
||||
{
|
||||
$row = self::fetchLogRow((int) $logId);
|
||||
if ($row === null) {
|
||||
return ['ok' => false, 'message' => '日志不存在'];
|
||||
}
|
||||
$context = self::contextFromLogRow($row);
|
||||
if (!empty($context['error'])) {
|
||||
return ['ok' => false, 'message' => $context['error']];
|
||||
}
|
||||
return self::execute($context, (int) $logId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @return array
|
||||
*/
|
||||
public static function prefillFromLog($logId)
|
||||
{
|
||||
$row = self::fetchLogRow((int) $logId);
|
||||
if ($row === null) {
|
||||
return ['ok' => false, 'message' => '日志不存在'];
|
||||
}
|
||||
$context = self::contextFromLogRow($row);
|
||||
if (!empty($context['error'])) {
|
||||
return ['ok' => false, 'message' => $context['error']];
|
||||
}
|
||||
return [
|
||||
'ok' => true,
|
||||
'log_id' => (int) $logId,
|
||||
'prefill' => [
|
||||
'config_name' => $context['config_name'],
|
||||
'ip' => $context['ip'],
|
||||
'domain' => $context['domain'],
|
||||
'page_url' => $context['page_url'],
|
||||
'referer' => $context['referer'],
|
||||
'user_agent' => $context['user_agent'],
|
||||
'accept_language' => $context['accept_language'],
|
||||
'visit_md5_code' => $context['visit_md5_code'],
|
||||
'cookies_json' => $context['cookies_json'],
|
||||
'fp_hdata' => $context['fp_hdata_raw'],
|
||||
'device' => $context['device_override'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $reason
|
||||
* @return string
|
||||
*/
|
||||
public static function tagReason($reason)
|
||||
{
|
||||
$reason = trim((string) $reason);
|
||||
if ($reason === '') {
|
||||
return self::REASON_PREFIX;
|
||||
}
|
||||
if (strpos($reason, self::REASON_PREFIX) === 0) {
|
||||
return $reason;
|
||||
}
|
||||
return self::REASON_PREFIX . ' ' . $reason;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $input
|
||||
* @return array
|
||||
*/
|
||||
private static function normalizeManualInput(array $input)
|
||||
{
|
||||
$configName = ConfigLoader::sanitizeConfigName($input['config_name'] ?? '');
|
||||
if (trim((string) ($input['config_name'] ?? '')) === '') {
|
||||
return ['error' => '请选择配置'];
|
||||
}
|
||||
|
||||
$domain = self::normalizeHost($input['domain'] ?? '');
|
||||
if ($domain === '') {
|
||||
$domain = self::defaultHostForConfig($configName);
|
||||
}
|
||||
if ($domain === '') {
|
||||
return ['error' => '请填写域名或绑定站点域名'];
|
||||
}
|
||||
|
||||
$pageUrl = trim((string) ($input['page_url'] ?? ''));
|
||||
if ($pageUrl === '') {
|
||||
$pageUrl = 'https://' . $domain . '/' . $configName . '.php';
|
||||
}
|
||||
|
||||
$cookies = [];
|
||||
if (!empty($input['cookies_json'])) {
|
||||
$decoded = json_decode((string) $input['cookies_json'], true);
|
||||
if (!is_array($decoded)) {
|
||||
return ['error' => 'Cookie JSON 格式无效'];
|
||||
}
|
||||
$cookies = $decoded;
|
||||
}
|
||||
|
||||
$fpRaw = trim((string) ($input['fp_hdata'] ?? ''));
|
||||
|
||||
return [
|
||||
'config_name' => $configName,
|
||||
'ip' => trim((string) ($input['ip'] ?? '8.8.8.8')),
|
||||
'domain' => $domain,
|
||||
'page_url' => $pageUrl,
|
||||
'referer' => trim((string) ($input['referer'] ?? '')),
|
||||
'user_agent' => trim((string) ($input['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36')),
|
||||
'accept_language' => trim((string) ($input['accept_language'] ?? 'en-US,en;q=0.9')),
|
||||
'visit_md5_code' => trim((string) ($input['visit_md5_code'] ?? '')),
|
||||
'cookies' => $cookies,
|
||||
'cookies_json' => (string) ($input['cookies_json'] ?? ''),
|
||||
'fp_hdata_raw' => $fpRaw,
|
||||
'fp_hdata' => self::parseFpHdata($fpRaw),
|
||||
'device_override' => trim((string) ($input['device'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @return array
|
||||
*/
|
||||
private static function contextFromLogRow(array $row)
|
||||
{
|
||||
$domain = self::normalizeHost($row['domain'] ?? '');
|
||||
$pageUrl = trim((string) ($row['page'] ?? ''));
|
||||
$config = self::guessConfigName($pageUrl, $domain);
|
||||
|
||||
if ($config === '' && $domain !== '') {
|
||||
try {
|
||||
$pdo = DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdo);
|
||||
$domRow = DomainRepository::findByHostname($pdo, $domain);
|
||||
if ($domRow && !empty($domRow['config_name'])) {
|
||||
$config = trim($domRow['config_name']);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
}
|
||||
if ($config === '') {
|
||||
$config = 'index';
|
||||
}
|
||||
|
||||
$referer = trim((string) ($row['http_referer'] ?? ''));
|
||||
if ($referer === '') {
|
||||
$referer = trim((string) ($row['referer'] ?? ''));
|
||||
}
|
||||
|
||||
$visitCode = trim((string) ($row['visit_md5_code'] ?? ''));
|
||||
$cookies = [];
|
||||
if ($visitCode !== '' && $visitCode !== 'loss') {
|
||||
$cookies['gfuid'] = $visitCode;
|
||||
}
|
||||
|
||||
$fpRaw = trim((string) ($row['fp_hdata'] ?? ''));
|
||||
|
||||
return [
|
||||
'config_name' => $config,
|
||||
'ip' => trim((string) ($row['IP'] ?? '')),
|
||||
'domain' => $domain !== '' ? $domain : self::defaultHostForConfig($config),
|
||||
'page_url' => $pageUrl !== '' ? $pageUrl : ('https://' . ($domain ?: 'localhost') . '/' . $config . '.php'),
|
||||
'referer' => $referer,
|
||||
'user_agent' => trim((string) ($row['user_agent'] ?? 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36')),
|
||||
'accept_language' => trim((string) ($row['accept_language_raw'] ?? $row['language'] ?? 'en-US,en;q=0.9')),
|
||||
'visit_md5_code' => $visitCode,
|
||||
'cookies' => $cookies,
|
||||
'cookies_json' => $cookies !== [] ? json_encode($cookies, JSON_UNESCAPED_UNICODE) : '',
|
||||
'fp_hdata_raw' => $fpRaw,
|
||||
'fp_hdata' => self::parseFpHdata($fpRaw),
|
||||
'device_override' => trim((string) ($row['device'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $context
|
||||
* @param int|null $sourceLogId
|
||||
* @return array
|
||||
*/
|
||||
private static function execute(array $context, $sourceLogId)
|
||||
{
|
||||
if (!defined('DB_USERNAME')) {
|
||||
require_once dirname(__DIR__, 2) . '/cong.php';
|
||||
}
|
||||
require_once dirname(__DIR__, 2) . '/lib/bootstrap.php';
|
||||
|
||||
$backup = self::backupGlobals();
|
||||
|
||||
try {
|
||||
if (session_status() !== PHP_SESSION_ACTIVE) {
|
||||
session_start();
|
||||
}
|
||||
$_SESSION = [];
|
||||
|
||||
self::injectEnvironment($context);
|
||||
|
||||
$configName = ConfigLoader::loadByName($context['config_name']);
|
||||
|
||||
$GLOBALS['__cloak_simulation_on'] = true;
|
||||
$GLOBALS['__cloak_force_sync_log'] = true;
|
||||
$GLOBALS['__cloak_debug_on'] = false;
|
||||
|
||||
CloakPipelineTimer::init();
|
||||
|
||||
global $v_info, $reason, $config_name;
|
||||
$config_name = $configName;
|
||||
$v_info = new visitorInfo();
|
||||
$v_info->get_visitor_Info(COSTM_IP_SCORE, CHECK_KEY, SHOW_SITE_COUNTRY);
|
||||
$reason = '';
|
||||
|
||||
CheckPipeline::run();
|
||||
|
||||
if (!isset($_SESSION['check_result']) || $_SESSION['check_result'] === '') {
|
||||
$_SESSION['check_result'] = 'false';
|
||||
if ($reason === '') {
|
||||
$reason = '未产生判定结果,默认安全页';
|
||||
}
|
||||
}
|
||||
$reason = cloak_finalize_reason($reason, $_SESSION['check_result']);
|
||||
|
||||
$model = self::detectDevice($context);
|
||||
$logs = self::buildLogs($v_info, $reason, $model, $context, $sourceLogId);
|
||||
|
||||
$route = SimulationRouteResolver::dispatch([
|
||||
'v_info' => $v_info,
|
||||
'logs' => $logs,
|
||||
'config_name' => $configName,
|
||||
'reason' => $reason,
|
||||
'model' => $model,
|
||||
]);
|
||||
|
||||
$secondary = null;
|
||||
if (!empty($route['needs_secondary'])) {
|
||||
if (!empty($context['fp_hdata']) && is_array($context['fp_hdata'])) {
|
||||
$secondary = self::runSecondary((int) ($route['log_id'] ?? 0), $context);
|
||||
} else {
|
||||
$secondary = [
|
||||
'skipped' => true,
|
||||
'message' => '命中二次风控但未提供 fp_hdata,日志保持 wait 状态',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$timing = CloakPipelineTimer::finish();
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'new_log_id' => $route['log_id'] ?? null,
|
||||
'sim_source' => $sourceLogId,
|
||||
'primary' => [
|
||||
'result' => $_SESSION['check_result'] ?? '',
|
||||
'reason' => $reason,
|
||||
'branch' => $route['branch'] ?? '',
|
||||
],
|
||||
'secondary' => $secondary,
|
||||
'timing' => $timing,
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $e->getMessage(),
|
||||
];
|
||||
} finally {
|
||||
self::restoreGlobals($backup);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @param array $context
|
||||
* @return array|null
|
||||
*/
|
||||
private static function runSecondary($logId, array $context)
|
||||
{
|
||||
if ($logId <= 0) {
|
||||
return ['ok' => false, 'message' => '无效 log_id,无法执行二次风控'];
|
||||
}
|
||||
|
||||
$fp = $context['fp_hdata'];
|
||||
if (!is_array($fp)) {
|
||||
return ['ok' => false, 'message' => 'fp_hdata 无效'];
|
||||
}
|
||||
|
||||
$scheme = 'https://';
|
||||
$host = $context['domain'];
|
||||
$fp['log_id'] = $logId;
|
||||
$fp['ip'] = $context['ip'];
|
||||
$fp['referer'] = $context['referer'];
|
||||
if (empty($fp['domain'])) {
|
||||
$fp['domain'] = $scheme . $host . '/';
|
||||
}
|
||||
|
||||
$result = FCheckHandler::process($fp);
|
||||
if (empty($result['ok'])) {
|
||||
return [
|
||||
'ok' => false,
|
||||
'message' => $result['message'] ?? '二次风控失败',
|
||||
];
|
||||
}
|
||||
|
||||
if (!empty($GLOBALS['__cloak_simulation_on']) && isset($result['reason'])) {
|
||||
self::tagReasonOnLog($logId, (string) $result['reason']);
|
||||
}
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'result' => !empty($result['result']),
|
||||
'reason' => $result['reason'] ?? '',
|
||||
'link' => $result['link'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @param string $reason
|
||||
* @param bool $passed
|
||||
*/
|
||||
private static function tagReasonOnLog($logId, $reason)
|
||||
{
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if (!$conn || $conn->connect_error) {
|
||||
return;
|
||||
}
|
||||
$reasonEsc = cloak_db_escape($conn, self::tagReason($reason));
|
||||
$conn->query("UPDATE `visitor_logs` SET `reason`='{$reasonEsc}' WHERE `id`=" . (int) $logId);
|
||||
$conn->close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $context
|
||||
*/
|
||||
private static function injectEnvironment(array $context)
|
||||
{
|
||||
$host = $context['domain'];
|
||||
$pageUrl = $context['page_url'];
|
||||
$parts = parse_url($pageUrl);
|
||||
$path = $parts['path'] ?? ('/' . $context['config_name'] . '.php');
|
||||
$query = $parts['query'] ?? '';
|
||||
|
||||
$_GET = [];
|
||||
if ($query !== '') {
|
||||
parse_str($query, $_GET);
|
||||
}
|
||||
$_REQUEST = $_GET;
|
||||
$_COOKIE = is_array($context['cookies']) ? $context['cookies'] : [];
|
||||
|
||||
$visitCode = $context['visit_md5_code'];
|
||||
if ($visitCode === '' || $visitCode === 'loss') {
|
||||
$visitCode = md5($context['ip'] . microtime(true));
|
||||
}
|
||||
$_COOKIE['gfuid'] = $visitCode;
|
||||
$_SESSION['gcuid'] = $visitCode;
|
||||
$_SESSION['gcu_ip'] = $context['ip'];
|
||||
|
||||
$_SERVER['HTTP_HOST'] = $host;
|
||||
$_SERVER['SERVER_NAME'] = $host;
|
||||
$_SERVER['REMOTE_ADDR'] = $context['ip'];
|
||||
$_SERVER['HTTP_USER_AGENT'] = $context['user_agent'];
|
||||
$_SERVER['HTTP_REFERER'] = $context['referer'];
|
||||
$_SERVER['HTTP_ACCEPT_LANGUAGE'] = $context['accept_language'];
|
||||
$_SERVER['HTTPS'] = 'on';
|
||||
$_SERVER['SERVER_PORT'] = '443';
|
||||
$_SERVER['REQUEST_URI'] = $path . ($query !== '' ? '?' . $query : '');
|
||||
$_SERVER['QUERY_STRING'] = $query;
|
||||
$_SERVER['PHP_SELF'] = $path;
|
||||
$_SERVER['SCRIPT_NAME'] = $path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param visitorInfo $v_info
|
||||
* @param string $reason
|
||||
* @param string $model
|
||||
* @param array $context
|
||||
* @param int|null $sourceLogId
|
||||
* @return array
|
||||
*/
|
||||
private static function buildLogs($v_info, $reason, $model, array $context, $sourceLogId)
|
||||
{
|
||||
$logs = [
|
||||
'site_name' => str_replace('www.', '', $context['domain']),
|
||||
'customers_ip' => $v_info->v_ip,
|
||||
'country' => substr((string) $v_info->v_country, 0, 2),
|
||||
'v_Browser' => $v_info->v_Browser,
|
||||
'v_referer' => $v_info->v_referer,
|
||||
'Client' => $v_info->v_Client,
|
||||
'v_PageURL' => $v_info->v_curPageURL,
|
||||
'accept_language' => $v_info->accept_language,
|
||||
'visit_md5_code' => $_SESSION['gcuid'] ?? $v_info->visit_md5_code,
|
||||
'campagin_id' => $v_info->costm_ip_score,
|
||||
'result' => $_SESSION['check_result'] ?? 'false',
|
||||
'device' => $model,
|
||||
'reason' => self::tagReason($reason),
|
||||
'user_agent' => $context['user_agent'],
|
||||
'http_referer' => $context['referer'],
|
||||
'accept_language_raw' => $context['accept_language'],
|
||||
'is_simulation' => 1,
|
||||
];
|
||||
if ($sourceLogId) {
|
||||
$logs['sim_source_log_id'] = (int) $sourceLogId;
|
||||
}
|
||||
return $logs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $context
|
||||
* @return string
|
||||
*/
|
||||
private static function detectDevice(array $context)
|
||||
{
|
||||
if ($context['device_override'] !== '') {
|
||||
return $context['device_override'];
|
||||
}
|
||||
$uaPath = dirname(__DIR__, 2) . '/Mobile-Detect/src/MobileDetect.php';
|
||||
if (!is_file($uaPath)) {
|
||||
return 'PC';
|
||||
}
|
||||
require_once $uaPath;
|
||||
$detect = new \Detection\MobileDetect;
|
||||
$deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer');
|
||||
if ($deviceType === 'computer') {
|
||||
return 'PC';
|
||||
}
|
||||
$patterns = $deviceType === 'tablet' ? $detect::getTabletDevices() : $detect::getPhoneDevices();
|
||||
$model = ucfirst($deviceType);
|
||||
foreach ($patterns as $brand => $pattern) {
|
||||
if (preg_match('/' . $pattern . '/i', $context['user_agent'], $matches)) {
|
||||
$model = $brand . ' ' . (isset($matches[1]) ? $matches[1] : '');
|
||||
break;
|
||||
}
|
||||
}
|
||||
return trim($model) !== '' ? trim($model) : 'Mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $raw
|
||||
* @return array|null
|
||||
*/
|
||||
private static function parseFpHdata($raw)
|
||||
{
|
||||
$raw = trim((string) $raw);
|
||||
if ($raw === '') {
|
||||
return null;
|
||||
}
|
||||
if ($raw[0] === '{' || $raw[0] === '[') {
|
||||
$decoded = json_decode($raw, true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
$decodedString = base64_decode($raw, true);
|
||||
if ($decodedString === false) {
|
||||
return null;
|
||||
}
|
||||
$decoded = json_decode($decodedString, true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $pageUrl
|
||||
* @param string $domain
|
||||
* @return string
|
||||
*/
|
||||
private static function guessConfigName($pageUrl, $domain)
|
||||
{
|
||||
$path = parse_url($pageUrl, PHP_URL_PATH);
|
||||
if (is_string($path) && $path !== '') {
|
||||
$base = basename($path, '.php');
|
||||
if ($base !== '' && preg_match('/^[a-zA-Z0-9_-]+$/', $base)) {
|
||||
return $base;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $configName
|
||||
* @return string
|
||||
*/
|
||||
private static function defaultHostForConfig($configName)
|
||||
{
|
||||
try {
|
||||
$pdo = DomainRepository::pdo();
|
||||
DomainRepository::ensureTable($pdo);
|
||||
$host = DomainRepository::primaryHostnameForConfig($pdo, $configName);
|
||||
if ($host !== '') {
|
||||
return self::normalizeHost($host);
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
return self::normalizeHost($_SERVER['HTTP_HOST'] ?? 'localhost');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $host
|
||||
* @return string
|
||||
*/
|
||||
private static function normalizeHost($host)
|
||||
{
|
||||
$host = trim(strtolower((string) $host));
|
||||
$host = preg_replace('#^https?://#', '', $host);
|
||||
$host = explode('/', $host)[0];
|
||||
return preg_replace('/^www\./', '', $host);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @return array|null
|
||||
*/
|
||||
private static function fetchLogRow($logId)
|
||||
{
|
||||
if ($logId <= 0) {
|
||||
return null;
|
||||
}
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if (!$conn || $conn->connect_error) {
|
||||
return null;
|
||||
}
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
$sql = 'SELECT * FROM `visitor_logs` WHERE `id`=' . (int) $logId . ' LIMIT 1';
|
||||
$res = $conn->query($sql);
|
||||
if (!$res || $res->num_rows === 0) {
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
$row = $res->fetch_assoc();
|
||||
$res->free();
|
||||
$conn->close();
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
private static function backupGlobals()
|
||||
{
|
||||
return [
|
||||
'_SERVER' => $_SERVER,
|
||||
'_GET' => $_GET,
|
||||
'_REQUEST' => $_REQUEST,
|
||||
'_COOKIE' => $_COOKIE,
|
||||
'_SESSION' => $_SESSION,
|
||||
'GLOBALS' => [
|
||||
'__cloak_simulation_on' => $GLOBALS['__cloak_simulation_on'] ?? null,
|
||||
'__cloak_force_sync_log' => $GLOBALS['__cloak_force_sync_log'] ?? null,
|
||||
'__cloak_debug_on' => $GLOBALS['__cloak_debug_on'] ?? null,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $backup
|
||||
*/
|
||||
private static function restoreGlobals(array $backup)
|
||||
{
|
||||
$_SERVER = $backup['_SERVER'];
|
||||
$_GET = $backup['_GET'];
|
||||
$_REQUEST = $backup['_REQUEST'];
|
||||
$_COOKIE = $backup['_COOKIE'];
|
||||
$_SESSION = $backup['_SESSION'];
|
||||
foreach ($backup['GLOBALS'] as $key => $value) {
|
||||
if ($value === null) {
|
||||
unset($GLOBALS[$key]);
|
||||
} else {
|
||||
$GLOBALS[$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Regular → Executable
@@ -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`),
|
||||
|
||||
@@ -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';
|
||||
|
||||
Regular → Executable
Regular → Executable
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/cong.php';
|
||||
session_start();
|
||||
|
||||
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
unset($_SESSION['password']);
|
||||
unset($_SESSION['check_result']);
|
||||
echo("<script language=javascript>alert('密码错误!');window.location='table.php'; </script>");
|
||||
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');
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>模拟测试访问</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
|
||||
<style>
|
||||
:root { --log-primary:#16baaa; --log-bg:#f0f2f5; --log-border:#e8ecf1; }
|
||||
body { margin:0; background:var(--log-bg); font-family:system-ui,sans-serif; color:#2c3e50; }
|
||||
.page { max-width:960px; margin:0 auto; padding:24px 20px 48px; }
|
||||
.page-header { display:flex; justify-content:space-between; align-items:flex-start; gap:12px; margin-bottom:20px; flex-wrap:wrap; }
|
||||
.page-header h1 { margin:0; font-size:1.35rem; }
|
||||
.page-header p { margin:4px 0 0; color:#64748b; font-size:.85rem; }
|
||||
.log-nav a { margin-left:10px; color:#64748b; text-decoration:none; font-size:.85rem; }
|
||||
.log-nav a.active { color:var(--log-primary); font-weight:600; }
|
||||
.card { background:#fff; border:1px solid var(--log-border); border-radius:10px; padding:20px 22px; margin-bottom:16px; }
|
||||
.warn { background:#fff8e6; border:1px solid #ffeeba; color:#856404; padding:10px 14px; border-radius:8px; font-size:.85rem; margin-bottom:16px; }
|
||||
.field { margin-bottom:14px; }
|
||||
.field label { display:block; font-size:.82rem; color:#64748b; margin-bottom:4px; }
|
||||
.result-box { display:none; margin-top:16px; padding:14px; background:#f8fafc; border-radius:8px; border:1px solid var(--log-border); font-size:.88rem; }
|
||||
.result-box pre { margin:8px 0 0; white-space:pre-wrap; word-break:break-all; font-size:12px; }
|
||||
.source-badge { display:inline-block; background:#e8f4fd; color:#0c5460; padding:2px 8px; border-radius:4px; font-size:.78rem; margin-left:8px; }
|
||||
details summary { cursor:pointer; color:var(--log-primary); font-size:.85rem; margin-bottom:10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1>模拟测试访问<?php if ($fromLog > 0): ?><span class="source-badge">来源日志 #<?php echo $fromLog; ?></span><?php endif; ?></h1>
|
||||
<p>重建请求环境,完整跑判定链(含真实 API 与二次风控),并写入新日志</p>
|
||||
</div>
|
||||
<div class="log-nav">
|
||||
<a href="table.php">访问日志</a>
|
||||
<a href="simulate_test.php" class="active">模拟测试</a>
|
||||
<a href="dashboard.php">配置中心</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="warn">此操作会调用真实 byApi / byApiRisk 接口,并写入 visitor_logs(标记为模拟测试)。</div>
|
||||
|
||||
<div class="card">
|
||||
<form id="sim-form" class="layui-form">
|
||||
<input type="hidden" name="from_log_id" id="from_log_id" value="<?php echo $fromLog; ?>">
|
||||
<div class="layui-row layui-col-space16">
|
||||
<div class="layui-col-md6 field">
|
||||
<label>配置 *</label>
|
||||
<select name="config_name" id="config_name" lay-verify="required">
|
||||
<option value="">请选择</option>
|
||||
<?php foreach ($configList as $cfg): ?>
|
||||
<option value="<?php echo sim_h($cfg); ?>"><?php echo sim_h($cfg); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="layui-col-md6 field">
|
||||
<label>IP *</label>
|
||||
<input type="text" name="ip" id="ip" class="layui-input" placeholder="如 154.28.212.171" lay-verify="required">
|
||||
</div>
|
||||
<div class="layui-col-md6 field">
|
||||
<label>域名(HTTP Host)</label>
|
||||
<input type="text" name="domain" id="domain" class="layui-input" placeholder="如 example.com">
|
||||
</div>
|
||||
<div class="layui-col-md12 field">
|
||||
<label>访问 URL(含广告参数)</label>
|
||||
<input type="text" name="page_url" id="page_url" class="layui-input" placeholder="https://example.com/index.php?fbclid=...">
|
||||
</div>
|
||||
<div class="layui-col-md12 field">
|
||||
<label>Referer</label>
|
||||
<input type="text" name="referer" id="referer" class="layui-input" placeholder="https://www.facebook.com/">
|
||||
</div>
|
||||
<div class="layui-col-md12 field">
|
||||
<label>User-Agent</label>
|
||||
<input type="text" name="user_agent" id="user_agent" class="layui-input">
|
||||
</div>
|
||||
<div class="layui-col-md6 field">
|
||||
<label>Accept-Language</label>
|
||||
<input type="text" name="accept_language" id="accept_language" class="layui-input" value="en-US,en;q=0.9">
|
||||
</div>
|
||||
<div class="layui-col-md6 field">
|
||||
<label>访客标识 visit_md5_code</label>
|
||||
<input type="text" name="visit_md5_code" id="visit_md5_code" class="layui-input" placeholder="留空自动生成">
|
||||
</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary>高级选项(Cookie / 二次风控 fp_hdata)</summary>
|
||||
<div class="layui-row layui-col-space16" style="margin-top:12px;">
|
||||
<div class="layui-col-md12 field">
|
||||
<label>Cookie JSON</label>
|
||||
<textarea name="cookies_json" id="cookies_json" class="layui-textarea" rows="3" placeholder='{"gfuid":"..."}'></textarea>
|
||||
</div>
|
||||
<div class="layui-col-md12 field">
|
||||
<label>fp_hdata(JSON 或 base64,用于二次风控)</label>
|
||||
<textarea name="fp_hdata" id="fp_hdata" class="layui-textarea" rows="5"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<div style="margin-top:18px;">
|
||||
<button type="button" class="layui-btn" id="btn-run" style="background:var(--log-primary);">执行模拟测试</button>
|
||||
<?php if ($fromLog > 0): ?>
|
||||
<button type="button" class="layui-btn layui-btn-normal" id="btn-replay">一键重放日志 #<?php echo $fromLog; ?></button>
|
||||
<?php endif; ?>
|
||||
<a href="table.php" class="layui-btn layui-btn-primary">返回日志</a>
|
||||
</div>
|
||||
</form>
|
||||
<div class="result-box" id="result-box"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/dist/layui.js"></script>
|
||||
<script src="/dist/jquery.min.js"></script>
|
||||
<script>
|
||||
layui.use(['form'], function(){
|
||||
var form = layui.form;
|
||||
form.render();
|
||||
|
||||
function showResult(html) {
|
||||
var box = document.getElementById('result-box');
|
||||
box.style.display = 'block';
|
||||
box.innerHTML = html;
|
||||
}
|
||||
|
||||
function renderResult(data) {
|
||||
if (!data.ok) {
|
||||
showResult('<strong style="color:#c0392b;">失败</strong>:' + (data.message || '未知错误'));
|
||||
return;
|
||||
}
|
||||
var html = '<strong style="color:#16baaa;">模拟完成</strong>';
|
||||
if (data.new_log_id) {
|
||||
html += ' · 新日志 <a href="table.php" target="_blank">#' + data.new_log_id + '</a>';
|
||||
}
|
||||
if (data.sim_source) {
|
||||
html += ' · 来源 #' + data.sim_source;
|
||||
}
|
||||
if (data.primary) {
|
||||
html += '<div style="margin-top:8px;">主流程:result=' + (data.primary.result || '') + ' · branch=' + (data.primary.branch || '') + '<br>理由:' + (data.primary.reason || '') + '</div>';
|
||||
}
|
||||
if (data.secondary) {
|
||||
if (data.secondary.skipped) {
|
||||
html += '<div style="margin-top:6px;color:#856404;">二次风控:' + (data.secondary.message || '跳过') + '</div>';
|
||||
} else if (data.secondary.ok) {
|
||||
html += '<div style="margin-top:6px;">二次风控:result=' + data.secondary.result + ' · ' + (data.secondary.reason || '') + '</div>';
|
||||
} else {
|
||||
html += '<div style="margin-top:6px;color:#c0392b;">二次风控失败:' + (data.secondary.message || '') + '</div>';
|
||||
}
|
||||
}
|
||||
html += '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
||||
showResult(html);
|
||||
}
|
||||
|
||||
function collectForm() {
|
||||
return {
|
||||
config_name: $('#config_name').val(),
|
||||
ip: $('#ip').val(),
|
||||
domain: $('#domain').val(),
|
||||
page_url: $('#page_url').val(),
|
||||
referer: $('#referer').val(),
|
||||
user_agent: $('#user_agent').val(),
|
||||
accept_language: $('#accept_language').val(),
|
||||
visit_md5_code: $('#visit_md5_code').val(),
|
||||
cookies_json: $('#cookies_json').val(),
|
||||
fp_hdata: $('#fp_hdata').val()
|
||||
};
|
||||
}
|
||||
|
||||
function loadPrefill(logId) {
|
||||
if (!logId) return;
|
||||
$.getJSON('datatable/simulate.php', { action: 'prefill', log_id: logId }, function(res) {
|
||||
if (!res.ok || !res.prefill) return;
|
||||
var p = res.prefill;
|
||||
$('#config_name').val(p.config_name || '');
|
||||
$('#ip').val(p.ip || '');
|
||||
$('#domain').val(p.domain || '');
|
||||
$('#page_url').val(p.page_url || '');
|
||||
$('#referer').val(p.referer || '');
|
||||
$('#user_agent').val(p.user_agent || '');
|
||||
$('#accept_language').val(p.accept_language || '');
|
||||
$('#visit_md5_code').val(p.visit_md5_code || '');
|
||||
$('#cookies_json').val(p.cookies_json || '');
|
||||
$('#fp_hdata').val(p.fp_hdata || '');
|
||||
form.render('select');
|
||||
});
|
||||
}
|
||||
|
||||
$('#btn-run').on('click', function() {
|
||||
var btn = this;
|
||||
btn.disabled = true;
|
||||
$.post('datatable/simulate.php?action=run', collectForm(), function(res) {
|
||||
renderResult(res);
|
||||
}, 'json').fail(function(xhr) {
|
||||
showResult('<strong style="color:#c0392b;">请求失败</strong> ' + xhr.status);
|
||||
}).always(function() { btn.disabled = false; });
|
||||
});
|
||||
|
||||
$('#btn-replay').on('click', function() {
|
||||
var logId = $('#from_log_id').val();
|
||||
if (!logId) return;
|
||||
var btn = this;
|
||||
btn.disabled = true;
|
||||
$.post('datatable/simulate.php?action=replay', { log_id: logId }, function(res) {
|
||||
renderResult(res);
|
||||
}, 'json').fail(function(xhr) {
|
||||
showResult('<strong style="color:#c0392b;">请求失败</strong> ' + xhr.status);
|
||||
}).always(function() { btn.disabled = false; });
|
||||
});
|
||||
|
||||
loadPrefill(<?php echo $fromLog; ?>);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -348,6 +348,7 @@ if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
</div>
|
||||
<div class="log-nav">
|
||||
<a href="table.php" class="active">访问日志</a>
|
||||
<a href="simulate_test.php">模拟测试</a>
|
||||
<a href="log_analytics.php">数据分析</a>
|
||||
<a href="dashboard.php">配置中心</a>
|
||||
</div>
|
||||
@@ -406,6 +407,7 @@ if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
|
||||
</div>
|
||||
<div class="log-actions">
|
||||
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
|
||||
<a href="simulate_test.php" class="layui-btn layui-btn-sm layui-btn-normal">模拟测试</a>
|
||||
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-col-settings" type="button">列设置</button>
|
||||
<button class="layui-btn layui-btn-sm btn-danger" id="btn-clear" lay-on="confirm" lay-event="footerDemoBtn2">清空数据库</button>
|
||||
<span class="log-count">当前查询数量:<strong id="total_nums">—</strong></span>
|
||||
@@ -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 '<div class="fp-hdata-cell">'
|
||||
+ '<span class="fp-hdata-summary text-muted">已记录</span>'
|
||||
+ '<span class="fp-hdata-actions"><span class="fp-hdata-btn fp-hdata-btn-json" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="' + escapeAttr(field) + '" data-title="' + escapeAttr(title || 'API 数据') + '">查看 JSON</span></span>'
|
||||
+ '</div>';
|
||||
}
|
||||
if (val == null || val === '' || val === '-') {
|
||||
return '<span class="text-muted">—</span>';
|
||||
}
|
||||
@@ -522,30 +530,23 @@ layui.use(function(){
|
||||
}
|
||||
|
||||
function cellFpHdata(d) {
|
||||
if (!d.fp_hdata && !d.fp_hdata_text) {
|
||||
if (!d.fp_hdata_text) {
|
||||
return '<span class="text-muted">—</span>';
|
||||
}
|
||||
var summary = d.fp_hdata_explain_summary || d.fp_hdata_text || '';
|
||||
var html = '';
|
||||
html += '<div class="fp-hdata-cell">';
|
||||
var summary = d.fp_hdata_text || '';
|
||||
var html = '<div class="fp-hdata-cell">';
|
||||
if (summary) {
|
||||
html += '<span class="fp-hdata-summary" title="' + escapeAttr(summary) + '">' + escapeHtml(summary.length > 48 ? summary.slice(0, 48) + '…' : summary) + '</span>';
|
||||
}
|
||||
html += '<span class="fp-hdata-actions">';
|
||||
if (d.fp_hdata_explain_html) {
|
||||
html += '<span class="fp-hdata-btn" data-explain-id="' + escapeAttr(String(d.id)) + '">判定详情</span>';
|
||||
window.__fpExplainCache = window.__fpExplainCache || {};
|
||||
window.__fpExplainCache[d.id] = d.fp_hdata_explain_html;
|
||||
}
|
||||
if (d.fp_hdata || d.fp_hdata_text) {
|
||||
html += '<span class="fp-hdata-btn fp-hdata-btn-raw" data-full="' + escapeAttr(d.fp_hdata || d.fp_hdata_text) + '">原始 hdata</span>';
|
||||
}
|
||||
html += '<span class="fp-hdata-btn fp-hdata-btn-raw" data-log-id="' + escapeAttr(String(d.id)) + '" data-field="fp_hdata">原始 hdata</span>';
|
||||
html += '</span></div>';
|
||||
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 '<span class="cell-mono" title="' + escapeHtml(d.visit_date) + '">' + escapeHtml(d.visit_date) + '</span>';
|
||||
@@ -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 '<span class="cloak-badge badge-info">是' + escapeHtml(String(src)) + '</span>';
|
||||
}
|
||||
return '<span class="text-muted">—</span>';
|
||||
}},
|
||||
{ field: 'sim_action', title: '操作', width: 108, defaultVisible: true, templet: function(d){
|
||||
return '<a class="layui-btn layui-btn-xs layui-btn-normal" href="simulate_test.php?from_log=' + encodeURIComponent(String(d.id)) + '">模拟测试</a>';
|
||||
}},
|
||||
];
|
||||
|
||||
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,7 +864,12 @@ 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]) {
|
||||
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,
|
||||
@@ -854,18 +879,65 @@ layui.use(function(){
|
||||
});
|
||||
return;
|
||||
}
|
||||
var full = this.getAttribute('data-full') || '';
|
||||
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: '<div style="padding:16px 18px;max-height:calc(85vh - 60px);overflow:auto;">' + res.html + '</div>'
|
||||
});
|
||||
}).fail(function() {
|
||||
layer.close(loadIdx);
|
||||
layer.msg('加载判定详情失败');
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
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: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + body + '</pre>'
|
||||
});
|
||||
}).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: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + body + '</pre>'
|
||||
content: '<pre style="padding:14px;margin:0;max-height:calc(70vh - 50px);overflow:auto;font-size:12px;white-space:pre-wrap;word-break:break-all;">' + bodyStatic + '</pre>'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
Regular → Executable
@@ -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, "<?php define('SHOW_SITE_MODE_SWITCH','ip_check');\n");
|
||||
$loadedScript = ConfigLoader::loadForRequest($key, 'unknown-host-no-mapping.test');
|
||||
$tests['script_priority_over_host'] = ($loadedScript === $key);
|
||||
@unlink($cfgFile);
|
||||
|
||||
$loadedIndex = ConfigLoader::loadForRequest('index', 'unknown-host-no-mapping.test');
|
||||
$tests['index_uses_host_fallback'] = ($loadedIndex === 'index');
|
||||
|
||||
$key = 'reg_test_' . substr(md5((string) time()), 0, 6);
|
||||
$cfgFile = $root . '/check_config/' . $key . '_config.php';
|
||||
file_put_contents($cfgFile, "<?php define('SHOW_SITE_MODE_SWITCH','ip_check');\n");
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* VisitorLogListHelper 回归(CLI JSON)
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require_once $root . '/cong.php';
|
||||
require_once $root . '/lib/DbHelper.php';
|
||||
require_once $root . '/lib/Cloak/VisitorLogListHelper.php';
|
||||
|
||||
$tests = [];
|
||||
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->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);
|
||||
Regular → Executable
+4
-1
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
/**
|
||||
* 二次风控 Session 缓存回归(CLI 输出 JSON)
|
||||
*/
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$root = dirname(__DIR__);
|
||||
require_once $root . '/cong.php';
|
||||
require_once $root . '/lib/CloakHttpHelpers.php';
|
||||
require_once $root . '/lib/Cloak/ClientIpResolver.php';
|
||||
require_once $root . '/lib/Cloak/RiskSecondaryCache.php';
|
||||
|
||||
$_SESSION = [];
|
||||
|
||||
$tests = [];
|
||||
|
||||
RiskSecondaryCache::store([
|
||||
'ip' => '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);
|
||||
Regular → Executable
Regular → Executable
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user