feat: initial commit
This commit is contained in:
Executable
+170
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/CloudflareConfig.php';
|
||||
|
||||
/**
|
||||
* Cloudflare API v4 客户端(Zone / DNS / SSL)
|
||||
*/
|
||||
class CloudflareClient
|
||||
{
|
||||
private $token;
|
||||
private $accountId;
|
||||
|
||||
public function __construct($token = null, $accountId = null)
|
||||
{
|
||||
$this->token = $token ?? CloudflareConfig::apiToken();
|
||||
$this->accountId = $accountId ?? CloudflareConfig::accountId();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function createZone($hostname)
|
||||
{
|
||||
$res = $this->request('POST', '/zones', [
|
||||
'name' => $hostname,
|
||||
'account' => ['id' => $this->accountId],
|
||||
'type' => 'full',
|
||||
]);
|
||||
if (!$res['ok'] && (int) ($res['code'] ?? 0) === 1061) {
|
||||
return $this->findZoneByName($hostname);
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function findZoneByName($hostname)
|
||||
{
|
||||
$path = '/zones?name=' . rawurlencode($hostname) . '&account.id=' . rawurlencode($this->accountId);
|
||||
$res = $this->request('GET', $path);
|
||||
if (!$res['ok']) {
|
||||
return $res;
|
||||
}
|
||||
$zones = $res['result'] ?? [];
|
||||
if (!is_array($zones) || empty($zones[0])) {
|
||||
return ['ok' => false, 'error' => 'Zone 不存在'];
|
||||
}
|
||||
return ['ok' => true, 'result' => $zones[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function getZone($zoneId)
|
||||
{
|
||||
return $this->request('GET', '/zones/' . rawurlencode($zoneId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function deleteZone($zoneId)
|
||||
{
|
||||
return $this->request('DELETE', '/zones/' . rawurlencode($zoneId));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function upsertARecord($zoneId, $fqdn, $ip, $proxied = true)
|
||||
{
|
||||
$path = '/zones/' . rawurlencode($zoneId) . '/dns_records?type=A&name=' . rawurlencode($fqdn);
|
||||
$list = $this->request('GET', $path);
|
||||
if (!$list['ok']) {
|
||||
return $list;
|
||||
}
|
||||
$records = is_array($list['result'] ?? null) ? $list['result'] : [];
|
||||
$existing = $records[0] ?? null;
|
||||
$payload = [
|
||||
'type' => 'A',
|
||||
'name' => $fqdn,
|
||||
'content' => $ip,
|
||||
'proxied' => (bool) $proxied,
|
||||
'ttl' => 1,
|
||||
];
|
||||
if (is_array($existing) && !empty($existing['id'])) {
|
||||
return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/dns_records/' . rawurlencode($existing['id']), $payload);
|
||||
}
|
||||
return $this->request('POST', '/zones/' . rawurlencode($zoneId) . '/dns_records', $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function setSslFlexible($zoneId)
|
||||
{
|
||||
return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/settings/ssl', ['value' => 'flexible']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
public function setAlwaysHttps($zoneId)
|
||||
{
|
||||
return $this->request('PATCH', '/zones/' . rawurlencode($zoneId) . '/settings/always_use_https', ['value' => 'on']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{ok:bool,result?:mixed,error?:string,code?:int,http_code?:int}
|
||||
*/
|
||||
private function request($method, $path, $body = null)
|
||||
{
|
||||
$url = 'https://api.cloudflare.com/client/v4' . $path;
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
||||
$headers = [
|
||||
'Authorization: Bearer ' . $this->token,
|
||||
'Content-Type: application/json',
|
||||
];
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
if ($body !== null) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
if (function_exists('cloak_curl_apply_upstream_defaults')) {
|
||||
cloak_curl_apply_upstream_defaults($ch, 5, 20);
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
}
|
||||
|
||||
$raw = curl_exec($ch);
|
||||
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
if ($raw === false) {
|
||||
$err = curl_error($ch);
|
||||
$errno = curl_errno($ch);
|
||||
curl_close($ch);
|
||||
self::logError($method, $path, 'curl', $errno . ':' . $err, $httpCode);
|
||||
return ['ok' => false, 'error' => 'API请求失败:' . $err . '(errno:' . $errno . ')', 'http_code' => $httpCode];
|
||||
}
|
||||
curl_close($ch);
|
||||
|
||||
if ($raw === '') {
|
||||
self::logError($method, $path, 'empty_body', '', $httpCode);
|
||||
return ['ok' => false, 'error' => 'API响应为空(HTTP ' . $httpCode . ')', 'http_code' => $httpCode];
|
||||
}
|
||||
|
||||
$decoded = json_decode($raw, true);
|
||||
if (!is_array($decoded)) {
|
||||
self::logError($method, $path, 'non_json', substr($raw, 0, 200), $httpCode);
|
||||
return ['ok' => false, 'error' => 'API响应非JSON(HTTP ' . $httpCode . ')', 'http_code' => $httpCode];
|
||||
}
|
||||
if (empty($decoded['success'])) {
|
||||
$msg = $decoded['errors'][0]['message'] ?? '未知错误';
|
||||
$code = (int) ($decoded['errors'][0]['code'] ?? 0);
|
||||
self::logError($method, $path, 'api_error', $msg, $httpCode);
|
||||
return ['ok' => false, 'error' => $msg, 'code' => $code, 'http_code' => $httpCode];
|
||||
}
|
||||
return ['ok' => true, 'result' => $decoded['result'] ?? null, 'http_code' => $httpCode];
|
||||
}
|
||||
|
||||
private static function logError($method, $path, $kind, $detail, $httpCode)
|
||||
{
|
||||
$line = date('Y-m-d H:i:s') . ' | Cloudflare ' . $method . ' ' . $path . ' | ' . $kind . ' | HTTP ' . $httpCode . ' | ' . $detail . "\n";
|
||||
@file_put_contents(dirname(__DIR__, 2) . '/err.txt', $line, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
Executable
+76
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Cloudflare 全局配置检测(cong.php 三项均为可选)
|
||||
*/
|
||||
class CloudflareConfig
|
||||
{
|
||||
/** @var array<string,mixed> CLI 回归用覆盖项(enabled / server_ip) */
|
||||
private static $testOverrides = [];
|
||||
|
||||
public static function setTestOverrides(array $overrides)
|
||||
{
|
||||
self::$testOverrides = $overrides;
|
||||
}
|
||||
|
||||
public static function clearTestOverrides()
|
||||
{
|
||||
self::$testOverrides = [];
|
||||
}
|
||||
|
||||
public static function apiToken()
|
||||
{
|
||||
return defined('CLOUDFLARE_API_TOKEN') ? trim((string) CLOUDFLARE_API_TOKEN) : '';
|
||||
}
|
||||
|
||||
public static function accountId()
|
||||
{
|
||||
return defined('CLOUDFLARE_ACCOUNT_ID') ? trim((string) CLOUDFLARE_ACCOUNT_ID) : '';
|
||||
}
|
||||
|
||||
public static function serverIp()
|
||||
{
|
||||
if (array_key_exists('server_ip', self::$testOverrides)) {
|
||||
return trim((string) self::$testOverrides['server_ip']);
|
||||
}
|
||||
return defined('SERVER_IP') ? trim((string) SERVER_IP) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Token + Account ID 均已配置时启用 Cloudflare 自动化
|
||||
*/
|
||||
public static function isEnabled()
|
||||
{
|
||||
if (array_key_exists('enabled', self::$testOverrides)) {
|
||||
return (bool) self::$testOverrides['enabled'];
|
||||
}
|
||||
return self::apiToken() !== '' && self::accountId() !== '';
|
||||
}
|
||||
|
||||
public static function statusLabel($status)
|
||||
{
|
||||
$map = [
|
||||
'local' => '本地登记',
|
||||
'pending_ns' => '待检测',
|
||||
'provisioning' => '配置中',
|
||||
'ready' => '已生效',
|
||||
'error' => '失败',
|
||||
];
|
||||
return $map[$status] ?? (string) $status;
|
||||
}
|
||||
|
||||
public static function parseNameservers($json)
|
||||
{
|
||||
if ($json === null || $json === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($json, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_cloudflare_enabled')) {
|
||||
function cloak_cloudflare_enabled()
|
||||
{
|
||||
return CloudflareConfig::isEnabled();
|
||||
}
|
||||
}
|
||||
Executable
+208
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
/**
|
||||
* 采集页 HTML 路径补全(cloak_* 为规范名,旧名为 deprecated 别名)
|
||||
*/
|
||||
|
||||
if (!function_exists('cloak_fetch_url_body')) {
|
||||
function cloak_fetch_url_body($url)
|
||||
{
|
||||
$ch = curl_init();
|
||||
$timeout = 30;
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 3);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
||||
$file_contents = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $file_contents;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_fetch_url_body */
|
||||
if (!function_exists('file_get_content_sstr')) {
|
||||
function file_get_content_sstr($url)
|
||||
{
|
||||
return cloak_fetch_url_body($url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_resolve_relative_url')) {
|
||||
/**
|
||||
* 将标签内相对 URL 解析为绝对地址(原 lIIIIl)
|
||||
*
|
||||
* @param string $l1 完整标签片段
|
||||
* @param string $l2 基准 URL
|
||||
*/
|
||||
function cloak_resolve_relative_url($l1, $l2)
|
||||
{
|
||||
$I2 = null;
|
||||
if (preg_match('/(.*)(href|src)\=(.+?)( |\/\>|\>).*/i', $l1, $regs)) {
|
||||
$I2 = $regs[3];
|
||||
}
|
||||
|
||||
if (!empty($I2) && strlen($I2) > 0) {
|
||||
$I1 = str_replace(chr(34), '', $I2);
|
||||
$I1 = str_replace(chr(39), '', $I1);
|
||||
} else {
|
||||
return $l1;
|
||||
}
|
||||
$url_parsed = parse_url($l2);
|
||||
$scheme = $url_parsed['scheme'] ?? '';
|
||||
if ($scheme != '') {
|
||||
$scheme = $scheme . '://';
|
||||
}
|
||||
$host = $url_parsed['host'] ?? '';
|
||||
$l3 = $scheme . $host;
|
||||
if (strlen($l3) == 0) {
|
||||
return $l1;
|
||||
}
|
||||
$path = dirname($url_parsed['path'] ?? '/');
|
||||
if (isset($path[0]) && $path[0] == '\\') {
|
||||
$path = '';
|
||||
}
|
||||
$pos = strpos($I1, '#');
|
||||
if ($pos > 0) {
|
||||
$I1 = substr($I1, 0, $pos);
|
||||
}
|
||||
|
||||
if (preg_match('/^(http|https|ftp):(\/\/|\\\\)(([\w\/\\\+\-~`@:%])+\.)+([\w\/\\\.\=\?\+\-~`@\':!%#]|(&)|&)+/i', $I1)) {
|
||||
return $l1;
|
||||
}
|
||||
if (isset($I1[0]) && $I1[0] == '/') {
|
||||
$I1 = $l3 . $I1;
|
||||
} elseif (substr($I1, 0, 3) == '../') {
|
||||
while (substr($I1, 0, 3) == '../') {
|
||||
$I1 = substr($I1, strlen($I1) - (strlen($I1) - 3), strlen($I1) - 3);
|
||||
if (strlen($path) > 0) {
|
||||
$path = dirname($path);
|
||||
}
|
||||
}
|
||||
$I1 = $l3 . $path . '/' . $I1;
|
||||
} elseif (substr($I1, 0, 2) == './') {
|
||||
$I1 = $l3 . $path . substr($I1, strlen($I1) - (strlen($I1) - 1), strlen($I1) - 1);
|
||||
} elseif (strtolower(substr($I1, 0, 7)) == 'mailto:' || strtolower(substr($I1, 0, 11)) == 'javascript:') {
|
||||
return $l1;
|
||||
} else {
|
||||
$I1 = $l3 . $path . '/' . $I1;
|
||||
}
|
||||
return str_replace($I2, '"' . $I1 . '"', $l1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_resolve_relative_url */
|
||||
if (!function_exists('lIIIIl')) {
|
||||
function lIIIIl($l1, $l2)
|
||||
{
|
||||
return cloak_resolve_relative_url($l1, $l2);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_format_url')) {
|
||||
function cloak_format_url($l1, $l2)
|
||||
{
|
||||
if (preg_match_all('/(<script[^>]+src=\"([^\"]+)\"[^>]*>)|(<link[^>]+href=\"([^\"]+)\"[^>]*>)|(<img[^>]+src=\"([^\"]+)\"[^>]*>)|(<a[^>]+href=\"([^\"]+)\"[^>]*>)|(<img[^>]+src=\'([^\']+)\'[^>]*>)|(<a[^>]+href=\'([^\']+)\'[^>]*>)/i', $l1, $regs)) {
|
||||
foreach ($regs[0] as $url) {
|
||||
$l1 = str_replace($url, cloak_resolve_relative_url($url, $l2), $l1);
|
||||
}
|
||||
}
|
||||
return $l1;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_format_url */
|
||||
if (!function_exists('formaturl')) {
|
||||
function formaturl($l1, $l2)
|
||||
{
|
||||
return cloak_format_url($l1, $l2);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_rewrite_safe_page_html')) {
|
||||
function cloak_rewrite_safe_page_html($content, $address_caiji, $website)
|
||||
{
|
||||
$domain = '/http.*\.\w*\//';
|
||||
preg_match($domain, $address_caiji, $res);
|
||||
$domain_caiji = '';
|
||||
foreach ($res as $domain_address) {
|
||||
$domain_caiji = $domain_address;
|
||||
}
|
||||
$surl = 'http://www.xxx.com/';
|
||||
$pattern = '/\'/';
|
||||
$content = preg_replace($pattern, '"', $content);
|
||||
$pattern = '/"\/\//';
|
||||
$content = preg_replace($pattern, '"http://', $content);
|
||||
$content = cloak_format_url($content, $surl);
|
||||
$pattern = '/Copyright.*<.*>/';
|
||||
$content = preg_replace($pattern, 'Copyright ' . $website . ' All rights reserved', $content);
|
||||
$pattern = '/copyright.*<.*>/';
|
||||
$content = preg_replace($pattern, 'Copyright ' . $website . ' All rights reserved', $content);
|
||||
$content = str_replace('http://www.xxx.com/', $domain_caiji, $content);
|
||||
$preg = '/<script[\s\S]*?<\/script>/i';
|
||||
return preg_replace($preg, '', $content, -1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_rewrite_safe_page_html */
|
||||
if (!function_exists('caiji_lujing_buquan')) {
|
||||
function caiji_lujing_buquan($content, $address_caiji, $website)
|
||||
{
|
||||
return cloak_rewrite_safe_page_html($content, $address_caiji, $website);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_rewrite_fp_page_html')) {
|
||||
function cloak_rewrite_fp_page_html($content, $address_caiji, $website)
|
||||
{
|
||||
$domain = '/http.*\.\w*\//';
|
||||
preg_match($domain, $address_caiji, $res);
|
||||
$domain_caiji = '';
|
||||
foreach ($res as $domain_address) {
|
||||
$domain_caiji = $domain_address;
|
||||
}
|
||||
$surl = 'http://www.xxx.com/';
|
||||
$pattern = '/\'/';
|
||||
$content = preg_replace($pattern, '"', $content);
|
||||
$pattern = '/"\/\//';
|
||||
$content = preg_replace($pattern, '"http://', $content);
|
||||
$content = cloak_format_url($content, $surl);
|
||||
$pattern = '/Copyright.*<.*>/';
|
||||
$content = preg_replace($pattern, 'Copyright ' . $website . ' All rights reserved', $content);
|
||||
$pattern = '/copyright.*<.*>/';
|
||||
$content = preg_replace($pattern, 'Copyright ' . $website . ' All rights reserved', $content);
|
||||
$content = str_replace('http://www.xxx.com/', $domain_caiji, $content);
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_rewrite_fp_page_html */
|
||||
if (!function_exists('caiji_lujing_buquan_fp')) {
|
||||
function caiji_lujing_buquan_fp($content, $address_caiji, $website)
|
||||
{
|
||||
return cloak_rewrite_fp_page_html($content, $address_caiji, $website);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_has_head_tag')) {
|
||||
function cloak_has_head_tag($html)
|
||||
{
|
||||
if (empty($html)) {
|
||||
return false;
|
||||
}
|
||||
$dom = new DOMDocument();
|
||||
libxml_use_internal_errors(true);
|
||||
$dom->loadHTML($html);
|
||||
libxml_clear_errors();
|
||||
return $dom->getElementsByTagName('head')->length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** @deprecated 使用 cloak_has_head_tag */
|
||||
if (!function_exists('hasHeadTag')) {
|
||||
function hasHeadTag($html)
|
||||
{
|
||||
return cloak_has_head_tag($html);
|
||||
}
|
||||
}
|
||||
Executable
+257
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/DomainRepository.php';
|
||||
require_once __DIR__ . '/CloudflareConfig.php';
|
||||
require_once __DIR__ . '/CloudflareClient.php';
|
||||
|
||||
/**
|
||||
* 域名添加 / NS 检测 / DNS+SSL 配置 / 删除编排
|
||||
*/
|
||||
class DomainProvisioningService
|
||||
{
|
||||
/** @var CloudflareClient|null CLI 回归注入 Mock 客户端 */
|
||||
private static $clientOverride = null;
|
||||
|
||||
public static function setClientOverride($client)
|
||||
{
|
||||
self::$clientOverride = $client;
|
||||
}
|
||||
|
||||
public static function clearClientOverride()
|
||||
{
|
||||
self::$clientOverride = null;
|
||||
}
|
||||
|
||||
private static function newClient()
|
||||
{
|
||||
return self::$clientOverride !== null ? self::$clientOverride : new CloudflareClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加域名:本地入库 + 可选创建 Cloudflare Zone
|
||||
*
|
||||
* @return array{ok:bool,error?:string,hostname?:string,id?:int,nameservers?:array,cf_status?:string}
|
||||
*/
|
||||
public static function provisionOnAdd(PDO $pdo, $hostname)
|
||||
{
|
||||
$add = DomainRepository::add($pdo, $hostname);
|
||||
if (!$add['ok']) {
|
||||
return $add;
|
||||
}
|
||||
|
||||
$id = (int) $add['id'];
|
||||
$host = $add['hostname'];
|
||||
|
||||
if (!CloudflareConfig::isEnabled()) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
||||
'cf_status' => 'local',
|
||||
]);
|
||||
return [
|
||||
'ok' => true,
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
'cf_status' => 'local',
|
||||
'nameservers' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$client = self::newClient();
|
||||
$zone = $client->createZone($host);
|
||||
if (!$zone['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => $zone['error'] ?? '创建 Zone 失败',
|
||||
]);
|
||||
return [
|
||||
'ok' => false,
|
||||
'error' => $zone['error'] ?? 'Cloudflare 创建 Zone 失败',
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
];
|
||||
}
|
||||
|
||||
$result = $zone['result'];
|
||||
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
|
||||
DomainRepository::updateCloudflareMeta($pdo, $id, [
|
||||
'cf_zone_id' => (string) ($result['id'] ?? ''),
|
||||
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
||||
'cf_status' => 'pending_ns',
|
||||
'cf_error' => '',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'id' => $id,
|
||||
'hostname' => $host,
|
||||
'cf_status' => 'pending_ns',
|
||||
'nameservers' => $ns,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动检测 NS 是否生效,生效后配置 A 记录与 SSL
|
||||
*
|
||||
* @return array{ok:bool,status?:string,message?:string,nameservers?:array}
|
||||
*/
|
||||
public static function checkAndProvision(PDO $pdo, $domainId)
|
||||
{
|
||||
$row = DomainRepository::findById($pdo, $domainId);
|
||||
if (!$row) {
|
||||
return ['ok' => false, 'message' => '域名不存在'];
|
||||
}
|
||||
|
||||
if (!CloudflareConfig::isEnabled()) {
|
||||
return ['ok' => false, 'message' => '未配置 Cloudflare API,无法检测'];
|
||||
}
|
||||
|
||||
$client = self::newClient();
|
||||
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
|
||||
|
||||
if ($zoneId === '') {
|
||||
$zone = $client->createZone($row['hostname']);
|
||||
if (!$zone['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => $zone['error'] ?? '补创建 Zone 失败',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $zone['error'] ?? '补创建 Zone 失败'];
|
||||
}
|
||||
$result = $zone['result'];
|
||||
$zoneId = (string) ($result['id'] ?? '');
|
||||
$ns = is_array($result['name_servers'] ?? null) ? $result['name_servers'] : [];
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_zone_id' => $zoneId,
|
||||
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
||||
'cf_status' => 'pending_ns',
|
||||
'cf_error' => '',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
$row = DomainRepository::findById($pdo, $domainId);
|
||||
}
|
||||
|
||||
$zoneRes = $client->getZone($zoneId);
|
||||
if (!$zoneRes['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => $zoneRes['error'] ?? '读取 Zone 失败',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $zoneRes['error'] ?? '读取 Zone 失败'];
|
||||
}
|
||||
|
||||
$zoneData = $zoneRes['result'];
|
||||
$status = strtolower((string) ($zoneData['status'] ?? 'pending'));
|
||||
$ns = CloudflareConfig::parseNameservers($row['cf_nameservers'] ?? '');
|
||||
if (empty($ns) && is_array($zoneData['name_servers'] ?? null)) {
|
||||
$ns = $zoneData['name_servers'];
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_nameservers' => json_encode($ns, JSON_UNESCAPED_UNICODE),
|
||||
]);
|
||||
}
|
||||
|
||||
if ($status !== 'active') {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'pending_ns',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
'cf_error' => '',
|
||||
]);
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => 'pending_ns',
|
||||
'message' => 'Nameserver 尚未生效,请到注册商修改为:' . implode('、', $ns),
|
||||
'nameservers' => $ns,
|
||||
];
|
||||
}
|
||||
|
||||
$serverIp = CloudflareConfig::serverIp();
|
||||
if ($serverIp === '') {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => 'SERVER_IP 未配置,无法添加 DNS 记录',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => 'SERVER_IP 未配置,请在 cong.php 中填写服务器公网 IP'];
|
||||
}
|
||||
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'provisioning',
|
||||
'cf_error' => '',
|
||||
]);
|
||||
|
||||
$host = $row['hostname'];
|
||||
foreach ([$host, 'www.' . $host] as $fqdn) {
|
||||
$dns = $client->upsertARecord($zoneId, $fqdn, $serverIp, true);
|
||||
if (!$dns['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => 'DNS 记录失败(' . $fqdn . '): ' . ($dns['error'] ?? ''),
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $dns['error'] ?? 'DNS 配置失败'];
|
||||
}
|
||||
}
|
||||
|
||||
$ssl = $client->setSslFlexible($zoneId);
|
||||
if (!$ssl['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => 'SSL Flexible 失败: ' . ($ssl['error'] ?? ''),
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $ssl['error'] ?? 'SSL 配置失败'];
|
||||
}
|
||||
|
||||
$https = $client->setAlwaysHttps($zoneId);
|
||||
if (!$https['ok']) {
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'error',
|
||||
'cf_error' => 'Always HTTPS 失败: ' . ($https['error'] ?? ''),
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
return ['ok' => false, 'message' => $https['error'] ?? 'Always HTTPS 配置失败'];
|
||||
}
|
||||
|
||||
DomainRepository::updateCloudflareMeta($pdo, $domainId, [
|
||||
'cf_status' => 'ready',
|
||||
'cf_error' => '',
|
||||
'cf_checked_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
|
||||
return [
|
||||
'ok' => true,
|
||||
'status' => 'ready',
|
||||
'message' => '域名已生效:A 记录(@/www) 已指向 ' . $serverIp . ',已开启 Flexible SSL 与 Always HTTPS',
|
||||
'nameservers' => $ns,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除本地记录并尝试删除 Cloudflare Zone
|
||||
*
|
||||
* @return array{ok:bool,message?:string,warning?:string}
|
||||
*/
|
||||
public static function deleteWithCloudflare(PDO $pdo, $domainId)
|
||||
{
|
||||
$row = DomainRepository::findById($pdo, $domainId);
|
||||
if (!$row) {
|
||||
return ['ok' => false, 'message' => '域名不存在'];
|
||||
}
|
||||
|
||||
$warning = '';
|
||||
$zoneId = trim((string) ($row['cf_zone_id'] ?? ''));
|
||||
if ($zoneId !== '' && CloudflareConfig::isEnabled()) {
|
||||
$del = self::newClient()->deleteZone($zoneId);
|
||||
if (!$del['ok']) {
|
||||
$warning = 'Cloudflare Zone 删除失败:' . ($del['error'] ?? '未知错误');
|
||||
}
|
||||
}
|
||||
|
||||
DomainRepository::deleteById($pdo, $domainId);
|
||||
return [
|
||||
'ok' => true,
|
||||
'message' => '域名已删除',
|
||||
'warning' => $warning,
|
||||
];
|
||||
}
|
||||
}
|
||||
Executable
+241
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
/**
|
||||
* 站点域名注册表(dashboard 管理,ip_check 按 Host 解析配置)
|
||||
*/
|
||||
class DomainRepository
|
||||
{
|
||||
public static function ensureTable(PDO $pdo)
|
||||
{
|
||||
$pdo->exec(
|
||||
"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 配置名',
|
||||
`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"
|
||||
);
|
||||
self::ensureCloudflareColumns($pdo);
|
||||
}
|
||||
|
||||
private static function ensureCloudflareColumns(PDO $pdo)
|
||||
{
|
||||
$columns = [
|
||||
'cf_zone_id' => "ADD COLUMN `cf_zone_id` varchar(64) DEFAULT NULL COMMENT 'Cloudflare Zone ID'",
|
||||
'cf_nameservers' => "ADD COLUMN `cf_nameservers` text DEFAULT NULL COMMENT 'Cloudflare NS JSON'",
|
||||
'cf_status' => "ADD COLUMN `cf_status` varchar(32) NOT NULL DEFAULT 'local' COMMENT 'Cloudflare 状态'",
|
||||
'cf_error' => "ADD COLUMN `cf_error` varchar(500) DEFAULT NULL COMMENT '最近错误'",
|
||||
'cf_checked_at' => "ADD COLUMN `cf_checked_at` datetime DEFAULT NULL COMMENT '上次检测时间'",
|
||||
];
|
||||
foreach ($columns as $name => $ddl) {
|
||||
if (!self::columnExists($pdo, 'cloak_site_domains', $name)) {
|
||||
$pdo->exec('ALTER TABLE `cloak_site_domains` ' . $ddl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function columnExists(PDO $pdo, $table, $column)
|
||||
{
|
||||
$st = $pdo->prepare(
|
||||
'SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ? AND COLUMN_NAME = ?'
|
||||
);
|
||||
$st->execute([DB_NAME, $table, $column]);
|
||||
return (int) $st->fetchColumn() > 0;
|
||||
}
|
||||
|
||||
private static function selectColumns()
|
||||
{
|
||||
return 'id, hostname, config_name, cf_zone_id, cf_nameservers, cf_status, cf_error, cf_checked_at, created_at';
|
||||
}
|
||||
|
||||
public static function pdo()
|
||||
{
|
||||
return new PDO(
|
||||
'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4',
|
||||
DB_USERNAME,
|
||||
DB_PASSWORD,
|
||||
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC]
|
||||
);
|
||||
}
|
||||
|
||||
public static function normalizeHostname($host)
|
||||
{
|
||||
$host = trim((string) $host);
|
||||
if ($host === '') {
|
||||
return '';
|
||||
}
|
||||
if (strpos($host, '://') !== false) {
|
||||
$parsed = parse_url($host, PHP_URL_HOST);
|
||||
$host = $parsed ?: $host;
|
||||
} else {
|
||||
$host = preg_replace('#/.*$#', '', $host);
|
||||
$host = preg_replace('#:\d+$#', '', $host);
|
||||
}
|
||||
$host = strtolower(trim($host, '.'));
|
||||
if (str_starts_with($host, 'www.')) {
|
||||
$host = substr($host, 4);
|
||||
}
|
||||
return $host;
|
||||
}
|
||||
|
||||
public static function hostToConfigKey($host)
|
||||
{
|
||||
$host = self::normalizeHostname($host);
|
||||
if ($host === '') {
|
||||
return 'index';
|
||||
}
|
||||
$key = preg_replace('/[^a-z0-9_-]+/i', '_', $host);
|
||||
$key = trim($key, '_');
|
||||
return $key !== '' ? $key : 'index';
|
||||
}
|
||||
|
||||
public static function listAll(PDO $pdo = null)
|
||||
{
|
||||
$pdo = $pdo ?: self::pdo();
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->query('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains ORDER BY hostname ASC');
|
||||
return $st->fetchAll();
|
||||
}
|
||||
|
||||
public static function findById(PDO $pdo, $id)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains WHERE id = ? LIMIT 1');
|
||||
$st->execute([(int) $id]);
|
||||
$row = $st->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public static function updateCloudflareMeta(PDO $pdo, $id, array $fields)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$allowed = ['cf_zone_id', 'cf_nameservers', 'cf_status', 'cf_error', 'cf_checked_at'];
|
||||
$sets = [];
|
||||
$vals = [];
|
||||
foreach ($allowed as $key) {
|
||||
if (array_key_exists($key, $fields)) {
|
||||
$sets[] = "`{$key}` = ?";
|
||||
$vals[] = $fields[$key];
|
||||
}
|
||||
}
|
||||
if ($sets === []) {
|
||||
return false;
|
||||
}
|
||||
$vals[] = (int) $id;
|
||||
$sql = 'UPDATE cloak_site_domains SET ' . implode(', ', $sets) . ' WHERE id = ?';
|
||||
$st = $pdo->prepare($sql);
|
||||
$st->execute($vals);
|
||||
return $st->rowCount() > 0;
|
||||
}
|
||||
|
||||
public static function findByHostname(PDO $pdo, $hostname)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$host = self::normalizeHostname($hostname);
|
||||
if ($host === '') {
|
||||
return null;
|
||||
}
|
||||
$st = $pdo->prepare('SELECT ' . self::selectColumns() . ' FROM cloak_site_domains WHERE hostname = ? LIMIT 1');
|
||||
$st->execute([$host]);
|
||||
$row = $st->fetch();
|
||||
return $row ?: null;
|
||||
}
|
||||
|
||||
public static function add(PDO $pdo, $hostname)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$host = self::normalizeHostname($hostname);
|
||||
if ($host === '' || !preg_match('/^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$/i', $host)) {
|
||||
return ['ok' => false, 'error' => '域名格式无效'];
|
||||
}
|
||||
try {
|
||||
$st = $pdo->prepare('INSERT INTO cloak_site_domains (hostname, config_name) VALUES (?, ?)');
|
||||
$st->execute([$host, '']);
|
||||
return ['ok' => true, 'id' => (int) $pdo->lastInsertId(), 'hostname' => $host];
|
||||
} catch (PDOException $e) {
|
||||
if ((int) $e->getCode() === 23000) {
|
||||
return ['ok' => false, 'error' => '该域名已存在'];
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public static function deleteById(PDO $pdo, $id)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('DELETE FROM cloak_site_domains WHERE id = ?');
|
||||
$st->execute([(int) $id]);
|
||||
return $st->rowCount() > 0;
|
||||
}
|
||||
|
||||
public static function bindConfigToDomain(PDO $pdo, $configName, $domainId)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$configName = trim($configName);
|
||||
$domainId = (int) $domainId;
|
||||
if ($configName === '' || $domainId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?")->execute([$configName]);
|
||||
$st = $pdo->prepare('UPDATE cloak_site_domains SET config_name = ? WHERE id = ?');
|
||||
$st->execute([$configName, $domainId]);
|
||||
return $st->rowCount() > 0;
|
||||
}
|
||||
|
||||
public static function clearConfigBinding(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare("UPDATE cloak_site_domains SET config_name = '' WHERE config_name = ?");
|
||||
$st->execute([trim($configName)]);
|
||||
}
|
||||
|
||||
public static function domainIdForConfig(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT id FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
|
||||
$st->execute([trim($configName)]);
|
||||
$row = $st->fetch();
|
||||
return $row ? (int) $row['id'] : 0;
|
||||
}
|
||||
|
||||
public static function primaryHostnameForConfig(PDO $pdo, $configName)
|
||||
{
|
||||
self::ensureTable($pdo);
|
||||
$st = $pdo->prepare('SELECT hostname FROM cloak_site_domains WHERE config_name = ? ORDER BY id ASC LIMIT 1');
|
||||
$st->execute([trim($configName)]);
|
||||
$row = $st->fetch();
|
||||
return $row ? $row['hostname'] : '';
|
||||
}
|
||||
|
||||
public static function resolveConfigNameForHost($host, $baseDir = null)
|
||||
{
|
||||
$baseDir = $baseDir ?: dirname(__DIR__, 2);
|
||||
$norm = self::normalizeHostname($host);
|
||||
if ($norm !== '') {
|
||||
try {
|
||||
$pdo = self::pdo();
|
||||
$row = self::findByHostname($pdo, $norm);
|
||||
if ($row && !empty($row['config_name'])) {
|
||||
$name = trim($row['config_name']);
|
||||
if (file_exists($baseDir . '/check_config/' . $name . '_config.php')) {
|
||||
return $name;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
}
|
||||
$key = self::hostToConfigKey($norm);
|
||||
if (file_exists($baseDir . '/check_config/' . $key . '_config.php')) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
return 'index';
|
||||
}
|
||||
}
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* 真实页 URL 解析(DB_FP 轮询 + KEEP_PARAMS)
|
||||
*/
|
||||
class FpUrlHelper
|
||||
{
|
||||
/**
|
||||
* 解析本次应跳转的真实页 URL(与 FpPageRenderer / f_check 逻辑一致)
|
||||
*
|
||||
* @param string $configName check_config 配置名(如 index)
|
||||
* @param bool $mutateNt 是否更新 nt.txt / nt Cookie(f_check 通过时为 true)
|
||||
* @return array{url:string,now_url:int}
|
||||
*/
|
||||
public static function resolve($configName, $mutateNt = true)
|
||||
{
|
||||
$all_fp_url = DB_FP;
|
||||
$num = is_array($all_fp_url) ? count($all_fp_url) : 1;
|
||||
$now_url = 0;
|
||||
|
||||
if ($num < 1) {
|
||||
return ['url' => is_string($all_fp_url) ? (string) $all_fp_url : '', 'now_url' => 0];
|
||||
}
|
||||
|
||||
if (isset($_COOKIE['nt']) && is_numeric($_COOKIE['nt'])) {
|
||||
$ntCookie = (int) $_COOKIE['nt'];
|
||||
if ($ntCookie >= 1 && $ntCookie <= $num) {
|
||||
$now_url = $ntCookie;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$now_url) {
|
||||
$match = false;
|
||||
$nt = 2;
|
||||
$ot = false;
|
||||
$handle = @fopen('nt.txt', 'r');
|
||||
if ($handle) {
|
||||
while (!feof($handle)) {
|
||||
$buffer = fgets($handle);
|
||||
$bb = explode('||||||', $buffer);
|
||||
if (trim($bb[0] ?? '') !== $configName) {
|
||||
continue;
|
||||
}
|
||||
if (!isset($bb[1]) || trim($bb[1]) === '') {
|
||||
continue;
|
||||
}
|
||||
$match = true;
|
||||
$ot = trim($bb[1]);
|
||||
$nt = trim($bb[1]);
|
||||
break;
|
||||
}
|
||||
fclose($handle);
|
||||
}
|
||||
|
||||
if ($match) {
|
||||
if ((int) $nt <= $num) {
|
||||
$now_url = max(1, (int) $ot);
|
||||
$nt = (int) $nt + 1;
|
||||
} else {
|
||||
$now_url = 1;
|
||||
$nt = 2;
|
||||
}
|
||||
} else {
|
||||
$now_url = 1;
|
||||
$nt = 2;
|
||||
}
|
||||
|
||||
if ($mutateNt) {
|
||||
write_nt_list($configName, $nt, $ot);
|
||||
}
|
||||
}
|
||||
|
||||
if (is_array($all_fp_url)) {
|
||||
$idx = max(0, min(max(1, (int) $now_url) - 1, $num - 1));
|
||||
$fp_url = $all_fp_url[$idx];
|
||||
$now_url = $idx + 1;
|
||||
} else {
|
||||
$fp_url = (string) $all_fp_url;
|
||||
}
|
||||
|
||||
if ($mutateNt) {
|
||||
setCrossDomainCookie('nt', $now_url, 3600 * 24 + time());
|
||||
}
|
||||
|
||||
if (defined('KEEP_PARAMS') && KEEP_PARAMS === 'ON' && !empty($_GET) && $fp_url !== '') {
|
||||
$params = http_build_query($_GET);
|
||||
$urlParts = parse_url($fp_url);
|
||||
if (isset($urlParts['query'])) {
|
||||
$fp_url .= '&' . $params;
|
||||
} else {
|
||||
$fp_url .= '?' . $params;
|
||||
}
|
||||
}
|
||||
|
||||
return ['url' => (string) $fp_url, 'now_url' => (int) $now_url];
|
||||
}
|
||||
|
||||
/**
|
||||
* 真实页 / 安全页兜底 URL(DB_FP 首项,否则 DB_ZP)
|
||||
*/
|
||||
public static function fallbackUrl()
|
||||
{
|
||||
$all = DB_FP;
|
||||
if (is_array($all)) {
|
||||
foreach ($all as $u) {
|
||||
if (trim((string) $u) !== '') {
|
||||
return (string) $u;
|
||||
}
|
||||
}
|
||||
} elseif (is_string($all) && trim($all) !== '') {
|
||||
return $all;
|
||||
}
|
||||
return defined('DB_ZP') ? (string) DB_ZP : '';
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* 指纹检测跳转页(原 check_flow 内联 HTML)
|
||||
*/
|
||||
class FingerprintRedirectRenderer
|
||||
{
|
||||
/**
|
||||
* @param string $configName 站点配置名,如 index
|
||||
*/
|
||||
public static function renderAndExit($configName)
|
||||
{
|
||||
$template = dirname(__DIR__, 3) . '/resources/fingerprint_redirect.html.php';
|
||||
if (!is_readable($template)) {
|
||||
return;
|
||||
}
|
||||
$html = file_get_contents($template);
|
||||
$html = str_replace('{{CONFIG_NAME}}', htmlspecialchars($configName, ENT_QUOTES, 'UTF-8'), $html);
|
||||
echo $html;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../FpUrlHelper.php';
|
||||
|
||||
/**
|
||||
* 真实页输出(原 page_6.php)
|
||||
*/
|
||||
class FpPageRenderer
|
||||
{
|
||||
/**
|
||||
* @param array $ctx 必须包含 logs 数组
|
||||
*/
|
||||
public static function render(array $ctx)
|
||||
{
|
||||
$logs = $ctx['logs'];
|
||||
|
||||
$my_url = $_SERVER['PHP_SELF'];
|
||||
$my_file_name = substr($my_url, strrpos($my_url, '/') + 1);
|
||||
$cong_name = str_replace('.php', '', $my_file_name);
|
||||
|
||||
$resolved = FpUrlHelper::resolve($cong_name, true);
|
||||
$fp_url = $resolved['url'];
|
||||
$logs['fp_url'] = $fp_url;
|
||||
write_log_db($logs);
|
||||
|
||||
$show_content = CLOAK_SHOW_CONTENT;
|
||||
if ($show_content == 'curl') {
|
||||
$html = file_get_content_sstr($fp_url);
|
||||
$_html = caiji_lujing_buquan_fp($html, $fp_url, $_SERVER['HTTP_HOST']);
|
||||
echo $_html;
|
||||
exit;
|
||||
}
|
||||
if ($show_content == '302') {
|
||||
header('location:' . $fp_url);
|
||||
exit;
|
||||
}
|
||||
if ($show_content == 'js') {
|
||||
?>
|
||||
<SCRIPT LANGUAGE="JavaScript">
|
||||
var time = 1;
|
||||
var timelong = 0;
|
||||
function diplaytime(){
|
||||
document.all.his.innerHTML = time -timelong ;
|
||||
timelong ++;
|
||||
}
|
||||
function redirect(){
|
||||
window.location.href="<?php echo $fp_url; ?>";
|
||||
}
|
||||
timer=setInterval('diplaytime()', 300);
|
||||
timer=setTimeout('redirect()',time * 300);
|
||||
</SCRIPT>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
/**
|
||||
* 二次风控页(原 page_666.php,依赖调用方 $logs 等变量)
|
||||
*/
|
||||
class RiskPageRenderer
|
||||
{
|
||||
/**
|
||||
* @param array $ctx 需含 v_info、log_id(write_log_db 返回值);可选 logs
|
||||
*/
|
||||
public static function render(array $ctx = [])
|
||||
{
|
||||
extract($ctx, EXTR_SKIP);
|
||||
require dirname(__DIR__, 3) . '/page_666.php';
|
||||
}
|
||||
}
|
||||
Executable
+63
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* 流量判定主流水线
|
||||
*
|
||||
* 阶段顺序(不可调换):
|
||||
* 1. Session 快速路径 → 2. 黑白名单 → 3. 广告来源限制 → 4. 临时链接 → 5. API/指纹
|
||||
*/
|
||||
class CheckPipeline
|
||||
{
|
||||
/**
|
||||
* 在调用方(ip_check 顶层)作用域执行完整判定链。
|
||||
* 依赖全局:$v_info, $config_name, $__cloak_debug_on, $reason
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function run()
|
||||
{
|
||||
global $v_info, $config_name, $__cloak_debug_on, $__cloak_debug_started, $reason;
|
||||
|
||||
if (!isset($reason)) {
|
||||
$reason = '';
|
||||
}
|
||||
|
||||
CloakPipelineTimer::init();
|
||||
|
||||
CloakPipelineTimer::stage('Session快速路径');
|
||||
if (self::resolveSessionFastPath()) {
|
||||
include __DIR__ . '/stages/resolve_session_fast_path_hit.inc.php';
|
||||
CloakPipelineTimer::finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, 'Session 快速路径', 'skip', '未命中缓存,进入完整判定流程(DEBUG 已重置 visit_to_*)');
|
||||
}
|
||||
|
||||
CloakPipelineTimer::stage('黑白名单');
|
||||
include __DIR__ . '/stages/apply_whitelist_blacklist.inc.php';
|
||||
CloakPipelineTimer::stage('广告来源限制');
|
||||
include __DIR__ . '/stages/run_ad_source_guard.inc.php';
|
||||
CloakPipelineTimer::stage('临时链接参数');
|
||||
include __DIR__ . '/stages/check_url_args_timeout.inc.php';
|
||||
include __DIR__ . '/stages/run_main_api_fingerprint.inc.php';
|
||||
CloakPipelineTimer::finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否命中 session 缓存结果(true = 跳过完整判定)
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function resolveSessionFastPath()
|
||||
{
|
||||
global $__cloak_debug_on;
|
||||
|
||||
return (
|
||||
!$__cloak_debug_on
|
||||
&& SHOW_SITE_MODE_SWITCH === 'ip_check'
|
||||
&& isset($_SESSION['check_result'])
|
||||
&& ($_SESSION['check_result'] === 'true' || $_SESSION['check_result'] === 'false')
|
||||
);
|
||||
}
|
||||
}
|
||||
Executable
+137
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../Page/FpPageRenderer.php';
|
||||
require_once __DIR__ . '/../Page/RiskPageRenderer.php';
|
||||
require_once __DIR__ . '/../FpUrlHelper.php';
|
||||
|
||||
/**
|
||||
* 判定完成后的路由输出(真实页 / 安全页 / 二次风控)
|
||||
*/
|
||||
class RouteResolver
|
||||
{
|
||||
/**
|
||||
* @param array $ctx 需含 v_info, logs, config_name, reason, model
|
||||
*/
|
||||
public static function dispatch(array $ctx)
|
||||
{
|
||||
extract($ctx, EXTR_SKIP);
|
||||
|
||||
// 正品模式:始终走安全页逻辑
|
||||
if (SHOW_SITE_MODE_SWITCH == 'zp') {
|
||||
self::renderSafePage($logs, $v_info);
|
||||
return;
|
||||
}
|
||||
|
||||
$showFp = (SHOW_SITE_MODE_SWITCH == 'ip_check' && $_SESSION['check_result'] != 'false')
|
||||
|| SHOW_SITE_MODE_SWITCH == 'fp';
|
||||
|
||||
if ($showFp) {
|
||||
self::renderFpOrRisk($logs, $v_info);
|
||||
return;
|
||||
}
|
||||
|
||||
self::renderSafePage($logs, $v_info);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs
|
||||
*/
|
||||
private static function renderSafePage(array $logs, $v_info)
|
||||
{
|
||||
if (SHOW_SITE_MODE_SWITCH == 'zp') {
|
||||
$logs['result'] = 'false';
|
||||
$logs['reason'] = '正品模式';
|
||||
}
|
||||
|
||||
$zp_url = DB_ZP;
|
||||
$logs['fp_url'] = $zp_url;
|
||||
write_log_db($logs);
|
||||
|
||||
if (empty($zp_url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$redirect_method = CLOAK_REDIRECT_METHOD;
|
||||
if ($redirect_method == 'curl') {
|
||||
$html = file_get_content_sstr($zp_url);
|
||||
$_html = caiji_lujing_buquan($html, $zp_url, $_SERVER['HTTP_HOST']);
|
||||
echo $_html;
|
||||
exit;
|
||||
}
|
||||
if ($redirect_method == '302') {
|
||||
header('Location: ' . $zp_url);
|
||||
exit;
|
||||
}
|
||||
if ($redirect_method == 'js') {
|
||||
self::echoJsRedirect($zp_url);
|
||||
}
|
||||
}
|
||||
|
||||
private static function renderFpOrRisk(array $logs, $v_info)
|
||||
{
|
||||
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 (($_SESSION['visit_to_3'] ?? '1') != '3'
|
||||
&& (CLOAK_RISK_NUMBER > 0 && CLOAK_RISK_NUMBER <= 90)) {
|
||||
// 二次风控:先写 wait 状态,f_check 完成后再更新 result / fp_url(避免 true+空跳转)
|
||||
$logs['result'] = 'wait';
|
||||
$logs['reason'] = '二次风控检测中';
|
||||
$logs['fp_url'] = '';
|
||||
$log_id = write_log_db($logs);
|
||||
$riskCtx = [
|
||||
'logs' => $logs,
|
||||
'v_info' => $v_info,
|
||||
'log_id' => $log_id,
|
||||
];
|
||||
if (CLOAK_REDIRECT_METHOD == 'curl') {
|
||||
self::renderRiskCurlInject($riskCtx);
|
||||
return;
|
||||
}
|
||||
RiskPageRenderer::render($riskCtx);
|
||||
return;
|
||||
}
|
||||
|
||||
$logs['result'] = 'true';
|
||||
$logs['reason'] = '';
|
||||
FpPageRenderer::render(['logs' => $logs]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $ctx 需含 v_info, log_id
|
||||
*/
|
||||
private static function renderRiskCurlInject(array $ctx)
|
||||
{
|
||||
extract($ctx, EXTR_SKIP);
|
||||
$zp_url = DB_ZP;
|
||||
$html = file_get_content_sstr($zp_url);
|
||||
$_html = caiji_lujing_buquan($html, $zp_url, $_SERVER['HTTP_HOST']);
|
||||
ob_start();
|
||||
require dirname(__DIR__, 3) . '/cloakjs.php';
|
||||
$js_to_inject = ob_get_clean();
|
||||
if (hasHeadTag($_html)) {
|
||||
$new_html = preg_replace('/(<head[^>]*>)/i', "$1\n" . $js_to_inject, $_html);
|
||||
} else {
|
||||
$new_html = $js_to_inject . $_html;
|
||||
}
|
||||
echo $new_html;
|
||||
exit;
|
||||
}
|
||||
|
||||
private static function echoJsRedirect($zp_url)
|
||||
{
|
||||
$zp_url_js = htmlspecialchars($zp_url, ENT_QUOTES, 'UTF-8');
|
||||
echo '<SCRIPT LANGUAGE="JavaScript">'
|
||||
. 'var time = 1;var timelong = 0;'
|
||||
. 'function diplaytime(){document.all.his.innerHTML = time -timelong ;timelong ++;}'
|
||||
. 'function redirect(){window.location.href="' . $zp_url_js . '";}'
|
||||
. "timer=setInterval('diplaytime()', 30);"
|
||||
. "timer=setTimeout('redirect()',time * 30);"
|
||||
. '</SCRIPT>';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
//指定ip显示仿品(优先数据库白名单组,其次兼容旧版 SHOW_SITE_IP 逗号列表)
|
||||
if (defined('WHITELIST_GROUP_ID') && (int) WHITELIST_GROUP_ID > 0) {
|
||||
$ips_fp_ary = ip_group_list_addresses((int) WHITELIST_GROUP_ID);
|
||||
} else {
|
||||
$ips_fp_ary = array_filter(array_map('trim', explode(',', SHOW_SITE_IP)), static function ($v) {
|
||||
return $v !== '';
|
||||
});
|
||||
}
|
||||
$reason = ""; // 判断理由
|
||||
|
||||
$whiteParamsMatched = false;
|
||||
if( !empty(WHITE_PARAMS) ) {
|
||||
$wparams = explode('=', WHITE_PARAMS, 2);
|
||||
|
||||
if(count($wparams) == 2) {
|
||||
$pk = $wparams[0];
|
||||
$pv = $wparams[1];
|
||||
}
|
||||
|
||||
if(isset($_GET[$pk]) && strip_tags($_GET[$pk]) == $pv) {
|
||||
$whiteParamsMatched = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
$match = false;
|
||||
if (defined('BLACKLIST_GROUP_ID') && (int) BLACKLIST_GROUP_ID > 0) {
|
||||
$blEntries = ip_group_list_addresses((int) BLACKLIST_GROUP_ID);
|
||||
$match = ip_visitor_in_blacklist_entries($v_info->v_ip, $blEntries);
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'黑名单检查', $match ? 'fail' : 'pass', $match ? 'IP 在黑名单组内' : '未命中黑名单', [
|
||||
'ip' => $v_info->v_ip,
|
||||
'group_id' => defined('BLACKLIST_GROUP_ID') ? BLACKLIST_GROUP_ID : 0,
|
||||
]);
|
||||
}
|
||||
if ($match) {
|
||||
$_SESSION["check_result"] = "false";
|
||||
$reason = "黑名单";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_record(__LINE__,'false', $reason, ['stage' => 'blacklist']);
|
||||
}
|
||||
}
|
||||
|
||||
if(in_array($v_info->v_ip, $ips_fp_ary) ) {
|
||||
$_SESSION["check_result"] = "true";
|
||||
$reason = "白名单";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'IP 白名单', 'pass', 'IP 在白名单组/列表中');
|
||||
cloak_dbg_record(__LINE__,'true', $reason, ['stage' => 'whitelist_ip']);
|
||||
}
|
||||
} elseif( !empty(WHITE_PARAMS) ) {
|
||||
$wparams = explode('=', WHITE_PARAMS, 2);
|
||||
if(count($wparams) == 2) {
|
||||
$pk = $wparams[0];
|
||||
$pv = $wparams[1];
|
||||
}
|
||||
if(isset($_GET[$pk]) && strip_tags($_GET[$pk]) == $pv) {
|
||||
$_SESSION["check_result"] = "true";
|
||||
$reason = "白名单链接参数";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'链接参数白名单', 'pass', WHITE_PARAMS);
|
||||
cloak_dbg_record(__LINE__,'true', $reason, ['stage' => 'whitelist_params']);
|
||||
}
|
||||
}
|
||||
} elseif ($__cloak_debug_on && !empty(WHITE_PARAMS)) {
|
||||
cloak_dbg_step(__LINE__,'链接参数白名单', 'skip', '参数不匹配: ' . WHITE_PARAMS);
|
||||
}
|
||||
+134
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
//判断临时链接参数(Guard 之后、API/指纹 之前)
|
||||
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 . ' 分钟,开始检查');
|
||||
}
|
||||
$dir = dirname(__DIR__, 4) . '/args';
|
||||
$pattern = '/^' . preg_quote($config_name, '/') . '_\d{10}\.txt$/i';
|
||||
$matchedFile = "";
|
||||
|
||||
if (is_dir($dir)) {
|
||||
$iterator = new DirectoryIterator($dir);
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && preg_match($pattern, $file->getFilename())) {
|
||||
$matchedFile = $file->getFilename();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$__url_args_cfg = CloakAdSourceGuard::configFromDefines();
|
||||
$__url_args_ctx = CloakAdSourceGuard::buildRequestContext();
|
||||
$__url_args_qualifies = CloakAdSourceGuard::shouldStartUrlArgsTimer($__url_args_cfg, $__url_args_ctx);
|
||||
|
||||
if ($__url_args_qualifies) {
|
||||
if(!$matchedFile) {
|
||||
$filename = $config_name . '_' . time() . '.txt';
|
||||
if(touch($dir . '/' . $filename)) {
|
||||
$matchedFile = $filename;
|
||||
}
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '临时链接参数', 'info', '命中广告来源规则,开始/续期有效时长', [
|
||||
'platforms_enabled' => CloakAdSourceGuard::anyPlatformEnabled($__url_args_cfg),
|
||||
]);
|
||||
}
|
||||
} elseif ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '临时链接参数', 'skip', '未通过广告来源规则,不启动计时');
|
||||
}
|
||||
|
||||
unset($__url_args_cfg, $__url_args_ctx, $__url_args_qualifies);
|
||||
|
||||
if($matchedFile) {
|
||||
if (preg_match('/_(\d{10})\.txt$/', $matchedFile, $matches)) {
|
||||
$fileTimestamp = $matches[1];
|
||||
$diffMinutes = floor((time() - $fileTimestamp) / 60);
|
||||
|
||||
if ($diffMinutes < CLOAK_URL_ARGS_TIMEOUT) {
|
||||
$check_result = false;
|
||||
$reason = "临时链接参数:距离第一次访问过去了" . $diffMinutes . " 分钟";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'临时链接参数', 'fail', $reason, ['diff_minutes' => $diffMinutes]);
|
||||
}
|
||||
} else {
|
||||
$check_result = true;
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'临时链接参数', 'pass', '已超过 ' . CLOAK_URL_ARGS_TIMEOUT . ' 分钟,继续后续判定');
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$check_result = false;
|
||||
$reason = "临时链接参数:尚未有效访问";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'临时链接参数', 'fail', $reason);
|
||||
}
|
||||
}
|
||||
|
||||
if(!$check_result) {
|
||||
$reason = cloak_reason_or($reason, '临时链接参数拦截');
|
||||
if (class_exists('CloakPipelineTimer', false)) {
|
||||
CloakPipelineTimer::finish();
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_record(__LINE__,'false', $reason, ['stage' => 'url_args_timeout']);
|
||||
CloakDebugPage::setExitPoint(__LINE__, '临时链接参数拦截 → 安全页(DEBUG 提前截停)');
|
||||
CloakDebugPage::renderAndExit([
|
||||
'config_name' => $config_name,
|
||||
'check_result' => 'false',
|
||||
'reason' => $reason,
|
||||
'visitor' => $v_info,
|
||||
'logs' => [],
|
||||
'device' => '',
|
||||
'mode' => SHOW_SITE_MODE_SWITCH,
|
||||
'fp_urls' => defined('DB_FP') ? DB_FP : [],
|
||||
'zp_url' => defined('DB_ZP') ? DB_ZP : '',
|
||||
'started_at' => $__cloak_debug_started,
|
||||
]);
|
||||
}
|
||||
$logs = array();
|
||||
$zp_url = DB_ZP;
|
||||
$logs['site_name'] = str_replace("www.", "", $_SERVER['HTTP_HOST']);
|
||||
$logs['customers_ip'] = $v_info->v_ip;
|
||||
$logs['country'] = substr($v_info->v_country, 0, 2);
|
||||
$logs['v_Browser'] = $v_info->v_Browser;
|
||||
$logs['v_referer'] = $v_info->v_referer;
|
||||
$logs['Client'] = $v_info->v_Client;
|
||||
$logs['v_PageURL'] = $v_info->v_curPageURL;
|
||||
$logs['v_site_server'] = $v_info->v_site_server;
|
||||
$logs['accept_language'] = $v_info->accept_language;
|
||||
$logs['visit_md5_code'] = $v_info->visit_md5_code;
|
||||
$logs['campagin_id'] = $v_info->costm_ip_score;
|
||||
$logs['result'] = "false";
|
||||
$logs['device'] = "";
|
||||
$logs['reason'] = $reason;
|
||||
$logs['fp_url'] = $zp_url;
|
||||
write_log_db($logs);
|
||||
|
||||
$redirect_method = CLOAK_REDIRECT_METHOD;
|
||||
if(!empty($zp_url)) {
|
||||
if($redirect_method == 'curl') {
|
||||
$html = file_get_content_sstr($zp_url);
|
||||
$_html = caiji_lujing_buquan($html, $zp_url, $_SERVER['HTTP_HOST']);
|
||||
echo $_html;
|
||||
exit;
|
||||
} elseif($redirect_method == '302') {
|
||||
header('Location: ' . $zp_url);
|
||||
exit;
|
||||
} elseif ($redirect_method == 'js') {
|
||||
$zp_url_js = htmlspecialchars($zp_url, ENT_QUOTES, 'UTF-8');
|
||||
echo '<SCRIPT LANGUAGE="JavaScript">'
|
||||
. 'var time = 1;var timelong = 0;'
|
||||
. 'function diplaytime(){document.all.his.innerHTML = time -timelong ;timelong ++;}'
|
||||
. 'function redirect(){window.location.href="' . $zp_url_js . '";}'
|
||||
. "timer=setInterval('diplaytime()', 30);"
|
||||
. "timer=setTimeout('redirect()',time * 30);"
|
||||
. '</SCRIPT>';
|
||||
exit;
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($__cloak_debug_on && CLOAK_URL_ARGS_TIMEOUT <= 0) {
|
||||
cloak_dbg_step(__LINE__,'临时链接参数', 'skip', 'CLOAK_URL_ARGS_TIMEOUT=0,已关闭');
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
// ── SESSION 快速路径命中 ──────────────────────────────────────────────
|
||||
// session 中已有缓存结果,跳过所有判断,直接进入路由输出
|
||||
$reason = '缓存';
|
||||
if ($__cloak_debug_on) {
|
||||
CloakDebugPage::setFlag('session_cached', true);
|
||||
cloak_dbg_step(__LINE__,'Session 快速路径', 'pass', '命中缓存 check_result=' . ($_SESSION['check_result'] ?? ''));
|
||||
cloak_dbg_record(__LINE__, $_SESSION['check_result'] ?? 'false', $reason, ['stage' => 'session_fast_path']);
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
// 广告来源限制(黑白名单之后、临时链接 / API 之前)
|
||||
if (empty($_SESSION['check_result'])) {
|
||||
$__ad_guard_cfg = [
|
||||
'ad_fb' => defined('CLOAK_AD_FB') && strtoupper(CLOAK_AD_FB) === 'ON',
|
||||
'ad_google' => defined('CLOAK_AD_GOOGLE') && strtoupper(CLOAK_AD_GOOGLE) === 'ON',
|
||||
'ad_tiktok' => defined('CLOAK_AD_TIKTOK') && strtoupper(CLOAK_AD_TIKTOK) === 'ON',
|
||||
'ad_strict' => defined('CLOAK_AD_STRICT') && strtoupper(CLOAK_AD_STRICT) === 'ON',
|
||||
'ad_spm_id' => defined('CLOAK_AD_SPM_ID') ? (string) CLOAK_AD_SPM_ID : '',
|
||||
];
|
||||
|
||||
if (CloakAdSourceGuard::anyPlatformEnabled($__ad_guard_cfg)) {
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '广告来源限制', 'info', '已启用平台检测,开始 evaluate()');
|
||||
}
|
||||
|
||||
$__ad_ref_raw = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '';
|
||||
$__ad_ref_parts = parse_url($__ad_ref_raw);
|
||||
$__ad_ref_q = [];
|
||||
if (!empty($__ad_ref_parts['query'])) {
|
||||
parse_str($__ad_ref_parts['query'], $__ad_ref_q);
|
||||
}
|
||||
$__ad_query = array_merge($__ad_ref_q, $_GET);
|
||||
|
||||
$__ad_result = CloakAdSourceGuard::evaluate($__ad_guard_cfg, [
|
||||
'referer' => $__ad_ref_raw,
|
||||
'query' => $__ad_query,
|
||||
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '',
|
||||
]);
|
||||
|
||||
if ($__ad_result['passed']) {
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '广告来源限制', 'pass', 'matched_by=' . $__ad_result['matched_by']);
|
||||
cloak_dbg_record(__LINE__, null, '', [
|
||||
'stage' => 'ad_source_guard',
|
||||
'matched_by' => $__ad_result['matched_by'],
|
||||
]);
|
||||
}
|
||||
} elseif ($__ad_guard_cfg['ad_strict']) {
|
||||
$_SESSION['check_result'] = 'false';
|
||||
$reason = $__ad_result['reason'];
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_record(__LINE__, 'false', $reason, [
|
||||
'stage' => 'ad_source_guard',
|
||||
'matched_by' => $__ad_result['matched_by'],
|
||||
'strict' => true,
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
$reason = cloak_reason_append($reason, $__ad_result['reason']);
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '广告来源限制', 'warn', $reason, [
|
||||
'matched_by' => $__ad_result['matched_by'],
|
||||
'strict' => false,
|
||||
]);
|
||||
cloak_dbg_record(__LINE__, null, $reason, [
|
||||
'stage' => 'ad_source_guard_uncertain',
|
||||
'matched_by' => $__ad_result['matched_by'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
unset($__ad_ref_raw, $__ad_ref_parts, $__ad_ref_q, $__ad_query, $__ad_result);
|
||||
} elseif ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '广告来源限制', 'skip', '未开启任何广告平台');
|
||||
}
|
||||
|
||||
unset($__ad_guard_cfg);
|
||||
} elseif ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, '广告来源限制', 'skip', 'check_result 已由前置步骤写入: ' . ($_SESSION['check_result'] ?? ''));
|
||||
}
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
// 主判定块:API 远程判定 + 前端指纹(Guard 之后执行)
|
||||
if(empty($_SESSION["check_result"]) ) // 缓存里没有判断结果或者判断结果是跳转安全站
|
||||
{
|
||||
if (class_exists('CloakPipelineTimer', false)) {
|
||||
CloakPipelineTimer::stage('API与指纹准备');
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'主判定块 (API+指纹)', 'info', 'check_result 为空,进入 API/指纹 判定块');
|
||||
}
|
||||
if(empty($_SESSION['visit_to_1'])) // 重置session信息
|
||||
{
|
||||
$_SESSION['visit_to_1'] = '1';
|
||||
$_SESSION['visit_to_2'] = '1';
|
||||
$_SESSION['visit_to_3'] = '1';
|
||||
$_SESSION["gcu_country"] = ''; // IP国家
|
||||
$_SESSION["gcu_Client"] = '';
|
||||
$_SESSION["gcu_ip"] = '';
|
||||
$_SESSION["gcu_Browser"] = '';
|
||||
}
|
||||
|
||||
if($_SESSION['visit_to_1'] == '1' && $_SESSION['visit_to_2'] != '2') // $_SESSION['visit_to_2'] 为2时表示已经判断过所有规则。
|
||||
{
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'主判定块 (API)', 'info', 'visit_to_1=1 且 visit_to_2!=2,执行 API 调用条件满足');
|
||||
}
|
||||
if(CLOAK_V_REFERER == "OFF" && $v_info->v_referer == "") {
|
||||
$v_info->v_referer = 'off';
|
||||
}
|
||||
|
||||
if(isset($_COOKIE['vbrowser']) && $_COOKIE['vbrowser'] == 'mobile') {
|
||||
$_SESSION["gcu_Browser"] = 'mobile';
|
||||
} else {
|
||||
$_SESSION["gcu_Browser"] = 'pc';
|
||||
}
|
||||
|
||||
if($v_info->v_referer != "") //排除机器访问,赋值给 _COOKIE , _SESSION
|
||||
{
|
||||
//echo $v_info->v_referer;
|
||||
$v_info->get_visit_md5_code(); //生成客户信息md5
|
||||
$v_referer_str = $v_info->v_referer; /* && $v_info->accept_language != "Chinese"*/
|
||||
if(!empty($v_referer_str) && !empty($v_info->accept_language) /* && $v_info->v_Browser != "unknown" && ( strpos($v_referer_str,"google") || strpos($v_referer_str,"bing") || strpos($v_referer_str,"yahoo") || strpos($v_referer_str,"facebook") || strpos($v_referer_str,"yandex")) */)
|
||||
{
|
||||
setCrossDomainCookie("gfuid", $v_info->visit_md5_code, time()+3600*24);
|
||||
$_SESSION["gcu_country"] = $v_info->v_country;
|
||||
$_SESSION["gcu_Client"] = $v_info->v_Client;
|
||||
$_SESSION["gcu_ip"] = $v_info->v_ip;
|
||||
$_SESSION["gcu_Browser"] = empty($_SESSION["gcu_Browser"])?$v_info->v_Browser:$_SESSION["gcu_Browser"];
|
||||
}
|
||||
$_SESSION["gcuid"] = $v_info->visit_md5_code;
|
||||
} elseif( !empty($_COOKIE["gfuid"]) ) {
|
||||
$_SESSION["gcuid"] = $_COOKIE["gfuid"];
|
||||
$v_info->v_referer = "COOKIE: " . cloak_current_request_url();
|
||||
$v_info->set_visit_md5_code($_COOKIE["gfuid"]); //赋值客户信息md5
|
||||
}
|
||||
|
||||
if(!empty($_SESSION["gcuid"]) && !empty($v_info->v_referer))
|
||||
{
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'Referer / gcuid', 'pass', 'gcuid 与 referer 均存在,可进入指纹/API 判定', [
|
||||
'gcuid' => $_SESSION['gcuid'],
|
||||
'referer' => $v_info->v_referer,
|
||||
]);
|
||||
}
|
||||
if(strtoupper(CLOAK_ZH_ON) == "ON" || strtoupper(CLOAK_MOBILE_ON) == "ON" || strtoupper(CLOAK_OS_ON) == "ON" || intval(IPHONE_MODEL) > 0 || intval(IS_VIRTUAL) > 0) {
|
||||
if (class_exists('CloakPipelineTimer', false)) {
|
||||
CloakPipelineTimer::stage('指纹检测');
|
||||
}
|
||||
|
||||
if (cloak_fingerprint_cookies_ready()) {
|
||||
if ($_COOKIE['cl'] == 'uklot' && is_numeric($_COOKIE['ctime'])) {
|
||||
$v_info->check_result = 'false';
|
||||
$mmr = cloak_fingerprint_mmr_reason();
|
||||
$reason = '可疑访客:' . $mmr;
|
||||
$_SESSION['check_result'] = trim($v_info->check_result);
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_record(__LINE__, trim($v_info->check_result), $reason, ['stage' => 'fingerprint_cookie', 'mmr' => $mmr]);
|
||||
}
|
||||
} else {
|
||||
$v_info->check_result = 'true';
|
||||
}
|
||||
} elseif (cloak_fingerprint_time_param_present()) {
|
||||
// 已完成指纹跳转(URL 含 time),但 Cookie 未写入:终止再次重定向
|
||||
$v_info->check_result = 'false';
|
||||
|
||||
$reason = cloak_reason_or($reason, '指纹Cookie未写入(已完成前端检测)');
|
||||
$_SESSION['check_result'] = 'false';
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, 'JS 指纹检测', 'fail', 'URL 含 time 但指纹 Cookie 缺失,停止重定向');
|
||||
cloak_dbg_record(__LINE__, 'false', $reason, ['stage' => 'fingerprint_cookie_missing']);
|
||||
}
|
||||
} else {
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__, 'JS 指纹检测', 'skip', 'DEBUG 跳过指纹 JS 跳转,直接进入 API 判定');
|
||||
CloakDebugPage::setFlag('skipped_fingerprint', true);
|
||||
$v_info->check_result = 'true';
|
||||
} else {
|
||||
FingerprintRedirectRenderer::renderAndExit($config_name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$v_info->check_result = 'true';
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'JS 指纹检测', 'skip', 'ZH/Mobile/OS/机型检测均为 OFF,跳过指纹');
|
||||
}
|
||||
}
|
||||
|
||||
if($v_info->check_result == 'true') {
|
||||
if (class_exists('CloakPipelineTimer', false)) {
|
||||
CloakPipelineTimer::stage('API远程判定');
|
||||
}
|
||||
$__api_return = $v_info->send_visitor_Info_xyz(); // 使用CLOAK判断接口
|
||||
if ($__cloak_debug_on) {
|
||||
if ($__api_return !== null) {
|
||||
CloakDebugPage::setApiResponse([
|
||||
'called' => true,
|
||||
'check_result'=> $v_info->check_result,
|
||||
'reason' => $v_info->v_reason,
|
||||
'country' => $v_info->v_country,
|
||||
'result_raw' => isset($__api_return['result']) ? ($__api_return['result'] ? 'true' : 'false') : '',
|
||||
'raw' => $__api_return,
|
||||
]);
|
||||
cloak_dbg_step(__LINE__,'API 远程判定', 'decide', 'API 已返回', [
|
||||
'api_result' => $v_info->check_result,
|
||||
'api_reason' => $v_info->v_reason !== '' ? $v_info->v_reason : '(空)',
|
||||
]);
|
||||
} else {
|
||||
CloakDebugPage::setApiResponse([
|
||||
'called' => false,
|
||||
'skip_reason' => '无法获取IP,未调用 API判断IP信息是否安全',
|
||||
]);
|
||||
cloak_dbg_step(__LINE__,'API 远程判定', 'skip', '前置条件不满足,未调用 API', [
|
||||
'v_ip' => $v_info->v_ip,
|
||||
'v_Browser' => $v_info->v_Browser,
|
||||
'accept_language' => $v_info->accept_language,
|
||||
]);
|
||||
}
|
||||
}
|
||||
//echo "<br />获取到结果:".$v_info->check_result."<br />";
|
||||
$_SESSION["gcu_referer"] = $v_info->v_referer;
|
||||
$_SESSION["gcu_country"] = $v_info->v_country;
|
||||
$_SESSION['visit_to_2'] = '2'; // 记录接口调用结果缓存
|
||||
|
||||
if ($__api_return !== null) {
|
||||
$_SESSION["check_result"] = trim($v_info->check_result);
|
||||
$reason = cloak_api_reason($__api_return, $_SESSION["check_result"]);
|
||||
} else {
|
||||
$_SESSION["check_result"] = "false";
|
||||
$reason = cloak_reason_or($reason, 'API未调用:缺少访客信息(ip/Browser/Accept-Language)');
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_record(__LINE__,$_SESSION['check_result'], $reason, [
|
||||
'stage' => 'api_cloak',
|
||||
'api_reason' => $v_info->v_reason,
|
||||
]);
|
||||
}
|
||||
} elseif ($v_info->check_result == 'false') {
|
||||
$_SESSION["check_result"] = "false";
|
||||
$reason = cloak_reason_or($reason, '指纹检测未通过,未调用API');
|
||||
}
|
||||
} else {
|
||||
$_SESSION["check_result"] = "false";
|
||||
$reason = "爬虫";
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'Referer / gcuid', 'fail', 'gcuid 或 referer 为空,判定为爬虫', [
|
||||
'gcuid' => $_SESSION['gcuid'] ?? '',
|
||||
'referer' => $v_info->v_referer,
|
||||
]);
|
||||
cloak_dbg_record(__LINE__,'false', $reason, ['stage' => 'no_referer_or_gcuid']);
|
||||
}
|
||||
}
|
||||
} elseif (($_SESSION['visit_to_2'] ?? '') == '2') {
|
||||
if (!empty($_SESSION["check_result"])) {
|
||||
if ($reason === '') {
|
||||
$reason = ($_SESSION['check_result'] === 'true')
|
||||
? 'Session API缓存:判定通过'
|
||||
: 'Session API缓存:判定拒绝';
|
||||
}
|
||||
} else {
|
||||
$_SESSION["check_result"] = 'false';
|
||||
$reason = cloak_reason_or($reason, 'Session状态异常(visit_to_2=2但无缓存结果),默认安全页');
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'主判定块 (API)', 'skip', 'visit_to_2=2,本块已跳过(' . $reason . ')');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($reason === '') {
|
||||
$reason = ($_SESSION['check_result'] === 'true')
|
||||
? '前置规则判定通过(主判定块已跳过)'
|
||||
: '前置规则判定拒绝(主判定块已跳过)';
|
||||
}
|
||||
if ($__cloak_debug_on) {
|
||||
cloak_dbg_step(__LINE__,'主判定块 (API)', 'skip', 'check_result 已有值,跳过主判定块');
|
||||
cloak_dbg_record(__LINE__,$_SESSION['check_result'] ?? 'false', $reason, ['stage' => 'session_partial']);
|
||||
}
|
||||
}
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
/**
|
||||
* 判定流水线各阶段耗时统计
|
||||
*/
|
||||
class CloakPipelineTimer
|
||||
{
|
||||
/** @var float */
|
||||
private static $startedAt = 0.0;
|
||||
|
||||
/** @var array<int, array{name:string,ms:float}> */
|
||||
private static $stages = [];
|
||||
|
||||
/** @var string */
|
||||
private static $currentStage = '';
|
||||
|
||||
/** @var float */
|
||||
private static $stageStartedAt = 0.0;
|
||||
|
||||
public static function init()
|
||||
{
|
||||
self::$startedAt = microtime(true);
|
||||
self::$stages = [];
|
||||
self::$currentStage = '';
|
||||
self::$stageStartedAt = 0.0;
|
||||
}
|
||||
|
||||
public static function stage($name)
|
||||
{
|
||||
self::endCurrentStage();
|
||||
self::$currentStage = (string) $name;
|
||||
self::$stageStartedAt = microtime(true);
|
||||
}
|
||||
|
||||
public static function finish()
|
||||
{
|
||||
self::endCurrentStage();
|
||||
$totalMs = round((microtime(true) - self::$startedAt) * 1000, 2);
|
||||
$payload = [
|
||||
'total_ms' => $totalMs,
|
||||
'stages' => self::$stages,
|
||||
];
|
||||
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
$GLOBALS['__cloak_judge_timing'] = $json;
|
||||
$GLOBALS['__cloak_judge_timing_text'] = self::formatText($payload);
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function summaryText()
|
||||
{
|
||||
if (!empty($GLOBALS['__cloak_judge_timing_text'])) {
|
||||
return (string) $GLOBALS['__cloak_judge_timing_text'];
|
||||
}
|
||||
if (!empty($GLOBALS['__cloak_judge_timing'])) {
|
||||
$decoded = json_decode((string) $GLOBALS['__cloak_judge_timing'], true);
|
||||
if (is_array($decoded)) {
|
||||
return self::formatText($decoded);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $payload
|
||||
*/
|
||||
public static function formatText(array $payload)
|
||||
{
|
||||
$parts = [];
|
||||
if (isset($payload['total_ms'])) {
|
||||
$parts[] = '总计 ' . $payload['total_ms'] . 'ms';
|
||||
}
|
||||
if (!empty($payload['stages']) && is_array($payload['stages'])) {
|
||||
foreach ($payload['stages'] as $stage) {
|
||||
if (!is_array($stage)) {
|
||||
continue;
|
||||
}
|
||||
$name = $stage['name'] ?? '';
|
||||
$ms = $stage['ms'] ?? 0;
|
||||
if ($name !== '') {
|
||||
$parts[] = $name . ' ' . $ms . 'ms';
|
||||
}
|
||||
}
|
||||
}
|
||||
return implode(' | ', $parts);
|
||||
}
|
||||
|
||||
private static function endCurrentStage()
|
||||
{
|
||||
if (self::$currentStage === '' || self::$stageStartedAt <= 0) {
|
||||
return;
|
||||
}
|
||||
self::$stages[] = [
|
||||
'name' => self::$currentStage,
|
||||
'ms' => round((microtime(true) - self::$stageStartedAt) * 1000, 2),
|
||||
];
|
||||
self::$currentStage = '';
|
||||
self::$stageStartedAt = 0.0;
|
||||
}
|
||||
}
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* 判定理由辅助
|
||||
*/
|
||||
|
||||
if (!function_exists('cloak_reason_append')) {
|
||||
function cloak_reason_append($reason, $suffix)
|
||||
{
|
||||
$reason = trim((string) $reason);
|
||||
$suffix = trim((string) $suffix);
|
||||
if ($suffix === '') {
|
||||
return $reason;
|
||||
}
|
||||
if ($reason === '') {
|
||||
return $suffix;
|
||||
}
|
||||
if (strpos($reason, $suffix) !== false) {
|
||||
return $reason;
|
||||
}
|
||||
return $reason . ';' . $suffix;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_reason_or')) {
|
||||
function cloak_reason_or($reason, $fallback)
|
||||
{
|
||||
$reason = trim((string) $reason);
|
||||
return $reason !== '' ? $reason : (string) $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_api_reason')) {
|
||||
function cloak_api_reason(array $return, $checkResult)
|
||||
{
|
||||
$reason = trim((string) ($return['reason'] ?? ''));
|
||||
if ($reason !== '') {
|
||||
return $reason;
|
||||
}
|
||||
$pass = ($checkResult === 'true' || $checkResult === true || !empty($return['result']));
|
||||
return $pass ? 'API判定通过' : 'API判定拒绝';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_finalize_reason')) {
|
||||
function cloak_finalize_reason($reason, $checkResult)
|
||||
{
|
||||
$reason = trim((string) $reason);
|
||||
if ($reason !== '') {
|
||||
return $reason;
|
||||
}
|
||||
$pass = ($checkResult === 'true');
|
||||
return $pass ? '判定通过(真实页)' : '判定拒绝(安全页)';
|
||||
}
|
||||
}
|
||||
Executable
+320
@@ -0,0 +1,320 @@
|
||||
<?php
|
||||
require_once dirname(__DIR__) . '/Cloak/ReasonHelper.php';
|
||||
|
||||
/**
|
||||
* 访客上下文与 API 判定(类名保留历史拼写 visitorInfo)
|
||||
*/
|
||||
class visitorInfo
|
||||
{
|
||||
/** @var string */
|
||||
public $v_ip;
|
||||
public $v_Browser;
|
||||
public $v_country;
|
||||
public $v_referer;
|
||||
public $v_Client;
|
||||
public $v_curPageURL;
|
||||
public $v_site_server;
|
||||
public $accept_language;
|
||||
public $visit_md5_code;
|
||||
public $costm_ip_score;
|
||||
public $check_key;
|
||||
public $check_country;
|
||||
public $check_result;
|
||||
public $v_reason;
|
||||
|
||||
/**
|
||||
* 从请求/Session 填充访客字段
|
||||
*
|
||||
* @param string $costm_ip_score 活动 ID(COSTM_IP_SCORE)
|
||||
* @param string $check_key API 密钥
|
||||
* @param string $check_country 国家过滤配置
|
||||
* @return void
|
||||
*/
|
||||
public function get_visitor_Info($costm_ip_score = '', $check_key = '', $check_country = '')
|
||||
{
|
||||
$this->v_country = !empty($_SESSION["gcu_country"])?$_SESSION["gcu_country"]:$check_country;
|
||||
$this->v_ip = $this->getIp();
|
||||
$this->v_Browser = $this->getBrowser();
|
||||
//$this->v_country = $this->findCityByIp($this->v_ip);
|
||||
$this->v_referer = $this->getFromPage();
|
||||
$this->v_Client = $this->v_isMobile();
|
||||
$this->v_curPageURL = $this->curPageURL();
|
||||
$this->v_site_server = $this->get_site_server();
|
||||
$this->accept_language = $this->get_accept_language();
|
||||
|
||||
$this->costm_ip_score = $costm_ip_score;
|
||||
$this->check_key = $check_key;
|
||||
$this->check_country = $check_country;
|
||||
|
||||
$_SESSION["gcu_country"] = $this->v_country;
|
||||
$_SESSION["gcu_Client"] = $this->v_Client;
|
||||
$_SESSION["gcu_ip"] = $this->v_ip;
|
||||
$_SESSION["gcu_Browser"] = $this->v_Browser;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用远程 cloak API 判定
|
||||
*
|
||||
* @return array|null API 解码结果;前置条件不足时 null
|
||||
*/
|
||||
public function send_visitor_Info_xyz()
|
||||
{
|
||||
if(!empty($_REQUEST["send"])&& $_REQUEST["send"] ==1)
|
||||
{
|
||||
print_r($jsonData); exit();
|
||||
}
|
||||
|
||||
if(!empty($this->v_ip))
|
||||
{
|
||||
$return = $this -> cloak_check_curl();
|
||||
if (!is_array($return)) {
|
||||
$return = ['result' => false, 'reason' => 'API响应异常', 'country' => $this->v_country];
|
||||
}
|
||||
$this->check_result = !empty($return['result']) ? "true" : "false";
|
||||
$this->v_country = isset($return['country']) ? $return['country'] : $this->v_country;
|
||||
$this->v_reason = cloak_api_reason($return, $this->check_result);
|
||||
return $return;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
function cloak_check_curl()
|
||||
{
|
||||
$headers = browser_headers();
|
||||
// $visit_domain = str_replace('www.', '', $_SERVER['HTTP_HOST']); // 当前网站域名
|
||||
// $visit_domain = (is_https() ? "https://" : "http://"). $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
|
||||
$jsonData['id'] = $this->costm_ip_score;
|
||||
$jsonData['ip'] = $this->v_ip;
|
||||
$jsonData['domain'] = $this->v_curPageURL;
|
||||
$jsonData['country_code'] = $this->check_country; //设置该参数后,将替换"广告策略》访问者地理位置>过滤"的设置,填写国家代码,多个用逗号分隔,如:US,GB,CA,AU,IE,NZ
|
||||
$jsonData['referer'] = get_SERVER_value('HTTP_REFERER');
|
||||
$jsonData['headers'] = json_encode($headers);
|
||||
$ch = curl_init('www.tiktokba.com/cloak/byApi');
|
||||
|
||||
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, ""); //Enables compression
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-type: application/json"]);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ["escloak-key: {$this->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"); //Forward response's cookies to visitor
|
||||
if ($_COOKIE) {//Forward visitor's cookie to our server
|
||||
curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies());
|
||||
}
|
||||
$return = curl_exec($ch);
|
||||
if ($return == false) {
|
||||
$curl_error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
return ['result' => false, 'reason' => 'API请求失败:' . $curl_error, 'country' => ''];
|
||||
}
|
||||
curl_close($ch);
|
||||
$decoded = json_decode($return, true);
|
||||
if (!is_array($decoded)) {
|
||||
return ['result' => false, 'reason' => 'API响应失败,请检查API密钥以及广告策略 ID', 'country' => ''];
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
// api.ttt.sh API查询国家代码
|
||||
public function get_ip_country($client_ip)
|
||||
{
|
||||
$str = "";
|
||||
//$url = "https://api.ttt.sh/ip/qqwry/".$client_ip."?type=addr";
|
||||
//$str = file_get_contents($url);
|
||||
return $str;
|
||||
}
|
||||
|
||||
public function set_visit_md5_code($code)
|
||||
{
|
||||
$this->visit_md5_code = $code;
|
||||
}
|
||||
|
||||
public function get_visit_md5_code()
|
||||
{
|
||||
$domain = str_replace("www.","",$_SERVER['HTTP_HOST']);
|
||||
$time = date("YmdHis");
|
||||
$ip = $this->getIp();
|
||||
$this->visit_md5_code = substr(md5($domain.$time.$ip) , 0 , 12 );
|
||||
|
||||
return $this->visit_md5_code;
|
||||
}
|
||||
|
||||
//获取访客ip
|
||||
public function getIp($type = 0)
|
||||
{
|
||||
$type = $type ? 1 : 0;
|
||||
static $ip = NULL;
|
||||
if ($ip !== NULL) return $ip[$type];
|
||||
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) { // 使用cloudflare 转发的IP地址
|
||||
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
|
||||
} else {
|
||||
if (getenv('HTTP_CLIENT_IP') && strcasecmp(getenv('HTTP_CLIENT_IP'), 'unknown')) {
|
||||
$ip = getenv('HTTP_CLIENT_IP');
|
||||
} elseif (getenv('HTTP_X_FORWARDED_FOR') && strcasecmp(getenv('HTTP_X_FORWARDED_FOR'), 'unknown')) {
|
||||
$ip = getenv('HTTP_X_FORWARDED_FOR');
|
||||
} elseif (getenv('REMOTE_ADDR') && strcasecmp(getenv('REMOTE_ADDR'), 'unknown')) {
|
||||
$ip = getenv('REMOTE_ADDR');
|
||||
} elseif (isset($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] && strcasecmp($_SERVER['REMOTE_ADDR'], 'unknown')) {
|
||||
$ip = $_SERVER['REMOTE_ADDR'];
|
||||
}
|
||||
}
|
||||
// if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
// $arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
// $pos = array_search('unknown',$arr);
|
||||
// if(false !== $pos) unset($arr[$pos]);
|
||||
// $ip = trim($arr[0]);
|
||||
// }elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
|
||||
// $ip = $_SERVER['HTTP_CLIENT_IP'];
|
||||
// }elseif (isset($_SERVER['REMOTE_ADDR'])) {
|
||||
// $ip = $_SERVER['REMOTE_ADDR'];
|
||||
// }
|
||||
// IP地址合法验证
|
||||
//$long = sprintf("%u",ip2long($ip));
|
||||
//$ip = $long ? array($ip, $long) : array('0.0.0.0', 0);
|
||||
|
||||
return $ip;
|
||||
}
|
||||
|
||||
//客户当前浏览的页面 url(浏览器实际访问地址,含正确 scheme 与 query)
|
||||
function curPageURL()
|
||||
{
|
||||
return cloak_current_request_url();
|
||||
}
|
||||
|
||||
//根据ip获取城市、网络运营商等信息
|
||||
public function findCityByIp($ip){
|
||||
$country_res = @file_get_contents("http://ip.taobao.com/service/getIpInfo.php?ip=".$ip);
|
||||
$country_json = json_decode($country_res,true);
|
||||
// Array ( [code] => 0 [data] => Array ( [ip] => 104.223.98.4 [country] => 美国 [area] => [region] => 德克萨斯 [city] => 达拉斯 [county] => XX [isp] => XX [country_id] => US [area_id] => [region_id] => US_143 [city_id] => US_1099 [county_id] => xx [isp_id] => xx ) )
|
||||
$country = $country_json["data"]["country"];
|
||||
|
||||
return $country;
|
||||
}
|
||||
|
||||
//site_server
|
||||
function get_site_server()
|
||||
{
|
||||
$dqml=getcwd();
|
||||
$dpml_ary = explode(DIRECTORY_SEPARATOR, $dqml);
|
||||
$dqml_str = $dpml_ary[2];
|
||||
$dqml_str = substr($dqml_str,0,4);
|
||||
|
||||
return $dqml_str;
|
||||
}
|
||||
|
||||
//字符串截取函数
|
||||
function ip_p_substr($p_bof,$p_eof,$p_str)
|
||||
{
|
||||
$p_1=explode($p_bof,$p_str);
|
||||
$p_e=strpos($p_1[1],$p_eof);
|
||||
$p_0=substr($p_1[1],0,$p_e);
|
||||
|
||||
return $p_0;
|
||||
}
|
||||
|
||||
//获取用户浏览器类型
|
||||
public function getBrowser(){
|
||||
$agent=$_SERVER["HTTP_USER_AGENT"];
|
||||
if(strpos($agent,'MSIE')!==false || strpos($agent,'rv:11.0')) //ie11判断
|
||||
return "ie";
|
||||
|
||||
else if(strpos($agent,'Firefox')!==false)
|
||||
return "firefox";
|
||||
|
||||
else if(strpos($agent,'Chrome')!==false)
|
||||
return "chrome";
|
||||
|
||||
else if(strpos($agent,'Opera')!==false)
|
||||
return 'opera';
|
||||
|
||||
else if((strpos($agent,'Chrome')==false)&&strpos($agent,'Safari')!==false)
|
||||
return 'safari';
|
||||
|
||||
else
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
//获取网站来源
|
||||
public function getFromPage(){
|
||||
if(empty($_SERVER['HTTP_REFERER'])) {
|
||||
return NULL;
|
||||
}
|
||||
else
|
||||
return $_SERVER['HTTP_REFERER'];
|
||||
}
|
||||
|
||||
function get_accept_language()
|
||||
{
|
||||
$lang = "";
|
||||
if(!empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) )
|
||||
{
|
||||
$accept_language = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 4); //只取前4位,这样只判断最优先的语言。如果取前5位,可能出现en,zh的情况,影响判断。
|
||||
if (preg_match("/zh-c/i", $accept_language))
|
||||
$lang = "Chinese";
|
||||
else if (preg_match("/zh/i", $accept_language))
|
||||
$lang = "Chinese_Trad";
|
||||
else if (preg_match("/en/i", $accept_language))
|
||||
$lang = "English";
|
||||
else if (preg_match("/fr/i", $accept_language))
|
||||
$lang = "French";
|
||||
else if (preg_match("/de/i", $accept_language))
|
||||
$lang = "German";
|
||||
else if (preg_match("/jp/i", $accept_language))
|
||||
$lang = "Japanese";
|
||||
else if (preg_match("/ko/i", $accept_language))
|
||||
$lang = "Korean";
|
||||
else if (preg_match("/es/i", $accept_language))
|
||||
$lang = "Spanish";
|
||||
else if (preg_match("/sv/i", $accept_language))
|
||||
$lang = "Swedish";
|
||||
else $lang = $_SERVER["HTTP_ACCEPT_LANGUAGE"];
|
||||
}
|
||||
|
||||
return $lang;
|
||||
}
|
||||
|
||||
/*移动端判断*/
|
||||
function v_isMobile()
|
||||
{
|
||||
// 如果有HTTP_X_WAP_PROFILE则一定是移动设备
|
||||
if (isset ($_SERVER['HTTP_X_WAP_PROFILE']))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// 如果via信息含有wap则一定是移动设备,部分服务商会屏蔽该信息
|
||||
if (isset ($_SERVER['HTTP_VIA']))
|
||||
{
|
||||
// 找不到为flase,否则为true
|
||||
return stristr($_SERVER['HTTP_VIA'], "wap") ? true : false;
|
||||
}
|
||||
// 脑残法,判断手机发送的客户端标志,兼容性有待提高
|
||||
if (isset ($_SERVER['HTTP_USER_AGENT']))
|
||||
{
|
||||
$clientkeywords = array ('nokia', 'sony', 'ericsson', 'mot', 'samsung', 'htc', 'sgh', 'lg', 'sharp', 'sie-', 'philips', 'panasonic', 'alcatel', 'lenovo', 'iphone', 'ipod', 'blackberry', 'meizu', 'android', 'netfront', 'symbian', 'ucweb', 'windowsce', 'palm', 'operamini', 'operamobi', 'openwave', 'nexusone', 'cldc', 'midp', 'wap', 'mobile'
|
||||
);
|
||||
// 从HTTP_USER_AGENT中查找手机浏览器的关键字
|
||||
if (preg_match("/(" . implode('|', $clientkeywords) . ")/i", strtolower($_SERVER['HTTP_USER_AGENT'])))
|
||||
{
|
||||
return "Mobile";
|
||||
}
|
||||
}
|
||||
// 协议法,因为有可能不准确,放到最后判断
|
||||
if (isset ($_SERVER['HTTP_ACCEPT']))
|
||||
{
|
||||
// 如果只支持wml并且不支持html那一定是移动设备
|
||||
// 如果支持wml和html但是wml在html之前则是移动设备
|
||||
if ((strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') !== false) && (strpos($_SERVER['HTTP_ACCEPT'], 'text/html') === false || (strpos($_SERVER['HTTP_ACCEPT'], 'vnd.wap.wml') < strpos($_SERVER['HTTP_ACCEPT'], 'text/html'))))
|
||||
{
|
||||
return "Mobile";
|
||||
}
|
||||
}
|
||||
return "PC";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
<?php
|
||||
/**
|
||||
* visitor_logs 聚合分析与结论生成
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/DbHelper.php';
|
||||
require_once __DIR__ . '/VisitorLogSchema.php';
|
||||
|
||||
class VisitorLogAnalytics
|
||||
{
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param array $filters date_from, date_to, domain, campagin_id
|
||||
* @return array
|
||||
*/
|
||||
public static function buildReport($conn, array $filters = [])
|
||||
{
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
VisitorLogSchema::ensureIndexes($conn);
|
||||
|
||||
$where = self::buildWhere($conn, $filters);
|
||||
$summary = self::fetchSummary($conn, $where);
|
||||
$byResult = self::fetchGroup($conn, $where, 'result', 'label');
|
||||
$byReason = self::fetchReasonTop($conn, $where, 15);
|
||||
$byCountry = self::fetchGroup($conn, $where, 'country', 'country', 10);
|
||||
$byHour = self::fetchByHour($conn, $where);
|
||||
$byDevice = self::fetchDeviceTop($conn, $where, 8);
|
||||
$timing = self::fetchTimingStats($conn, $where);
|
||||
$compare = self::fetchCompare($conn, $filters, $summary);
|
||||
$conclusions = self::buildConclusions($summary, $byReason, $byCountry, $byHour, $byDevice, $timing, $compare, $byResult);
|
||||
|
||||
return [
|
||||
'summary' => $summary,
|
||||
'by_result' => $byResult,
|
||||
'by_reason' => $byReason,
|
||||
'by_country' => $byCountry,
|
||||
'by_hour' => $byHour,
|
||||
'by_device' => $byDevice,
|
||||
'timing' => $timing,
|
||||
'compare' => $compare,
|
||||
'conclusions' => $conclusions,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param array $filters
|
||||
*/
|
||||
private static function buildWhere($conn, array $filters)
|
||||
{
|
||||
$parts = ['1=1'];
|
||||
|
||||
$dateFrom = trim((string) ($filters['date_from'] ?? ''));
|
||||
$dateTo = trim((string) ($filters['date_to'] ?? ''));
|
||||
if ($dateFrom === '' && $dateTo === '') {
|
||||
$dateTo = date('Y-m-d');
|
||||
$dateFrom = date('Y-m-d', strtotime('-6 days'));
|
||||
}
|
||||
if ($dateFrom !== '') {
|
||||
$parts[] = "`visit_date` >= '" . cloak_db_escape($conn, $dateFrom . ' 00:00:00') . "'";
|
||||
}
|
||||
if ($dateTo !== '') {
|
||||
$parts[] = "`visit_date` <= '" . cloak_db_escape($conn, $dateTo . ' 23:59:59') . "'";
|
||||
}
|
||||
|
||||
$domain = trim((string) ($filters['domain'] ?? ''));
|
||||
if ($domain !== '') {
|
||||
$parts[] = "`domain` LIKE '%" . cloak_db_escape($conn, $domain) . "%'";
|
||||
}
|
||||
|
||||
$campaginId = trim((string) ($filters['campagin_id'] ?? ''));
|
||||
if ($campaginId !== '') {
|
||||
$parts[] = "`campagin_id` = '" . cloak_db_escape($conn, $campaginId) . "'";
|
||||
}
|
||||
|
||||
return implode(' AND ', $parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
*/
|
||||
private static function fetchSummary($conn, $where)
|
||||
{
|
||||
$sql = "SELECT
|
||||
COUNT(*) AS total,
|
||||
SUM(CASE WHEN `result` = 'true' THEN 1 ELSE 0 END) AS pass_cnt,
|
||||
SUM(CASE WHEN `result` = 'false' THEN 1 ELSE 0 END) AS block_cnt,
|
||||
SUM(CASE WHEN `result` = 'wait' THEN 1 ELSE 0 END) AS wait_cnt
|
||||
FROM `visitor_logs` WHERE {$where}";
|
||||
$res = $conn->query($sql);
|
||||
$row = $res ? $res->fetch_assoc() : [];
|
||||
|
||||
$total = (int) ($row['total'] ?? 0);
|
||||
$pass = (int) ($row['pass_cnt'] ?? 0);
|
||||
$block = (int) ($row['block_cnt'] ?? 0);
|
||||
$wait = (int) ($row['wait_cnt'] ?? 0);
|
||||
$decided = max(1, $pass + $block);
|
||||
|
||||
return [
|
||||
'total' => $total,
|
||||
'pass' => $pass,
|
||||
'block' => $block,
|
||||
'wait' => $wait,
|
||||
'pass_rate' => $total > 0 ? round($pass / $decided * 100, 1) : 0,
|
||||
'block_rate' => $total > 0 ? round($block / $decided * 100, 1) : 0,
|
||||
'wait_rate' => $total > 0 ? round($wait / $total * 100, 1) : 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
* @param string $column
|
||||
* @param string $labelKey
|
||||
* @param int $limit
|
||||
*/
|
||||
private static function fetchGroup($conn, $where, $column, $labelKey, $limit = 0)
|
||||
{
|
||||
$limitSql = $limit > 0 ? ' LIMIT ' . (int) $limit : '';
|
||||
$sql = "SELECT `{$column}` AS grp, COUNT(*) AS cnt FROM `visitor_logs`
|
||||
WHERE {$where} GROUP BY `{$column}` ORDER BY cnt DESC{$limitSql}";
|
||||
$res = $conn->query($sql);
|
||||
$rows = [];
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$label = trim((string) ($r['grp'] ?? ''));
|
||||
if ($label === '') {
|
||||
$label = '(空)';
|
||||
}
|
||||
if ($column === 'result') {
|
||||
if ($label === 'true') {
|
||||
$label = '通过';
|
||||
} elseif ($label === 'false') {
|
||||
$label = '拦截';
|
||||
} elseif ($label === 'wait') {
|
||||
$label = '检测中';
|
||||
}
|
||||
}
|
||||
$rows[] = [
|
||||
$labelKey => $label,
|
||||
'count' => (int) $r['cnt'],
|
||||
];
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
* @param int $limit
|
||||
*/
|
||||
private static function fetchReasonTop($conn, $where, $limit)
|
||||
{
|
||||
$limit = (int) $limit;
|
||||
$sql = "SELECT IFNULL(NULLIF(TRIM(`reason`), ''), '(无理由/通过)') AS reason, COUNT(*) AS cnt
|
||||
FROM `visitor_logs` WHERE {$where} AND (`result` = 'false' OR `result` = 'wait')
|
||||
GROUP BY reason ORDER BY cnt DESC LIMIT {$limit}";
|
||||
$res = $conn->query($sql);
|
||||
$rows = [];
|
||||
$total = 0;
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$cnt = (int) $r['cnt'];
|
||||
$total += $cnt;
|
||||
$rows[] = [
|
||||
'reason' => (string) $r['reason'],
|
||||
'count' => $cnt,
|
||||
];
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
foreach ($rows as &$row) {
|
||||
$row['pct'] = $total > 0 ? round($row['count'] / $total * 100, 1) : 0;
|
||||
}
|
||||
unset($row);
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
*/
|
||||
private static function fetchByHour($conn, $where)
|
||||
{
|
||||
$sql = "SELECT HOUR(`visit_date`) AS hr, COUNT(*) AS cnt
|
||||
FROM `visitor_logs` WHERE {$where}
|
||||
GROUP BY hr ORDER BY hr ASC";
|
||||
$res = $conn->query($sql);
|
||||
$map = array_fill(0, 24, 0);
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$hr = (int) $r['hr'];
|
||||
if ($hr >= 0 && $hr <= 23) {
|
||||
$map[$hr] = (int) $r['cnt'];
|
||||
}
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
$rows = [];
|
||||
for ($h = 0; $h < 24; $h++) {
|
||||
$rows[] = ['hour' => sprintf('%02d:00', $h), 'count' => $map[$h]];
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
* @param int $limit
|
||||
*/
|
||||
private static function fetchDeviceTop($conn, $where, $limit)
|
||||
{
|
||||
$limit = (int) $limit;
|
||||
$sql = "SELECT IFNULL(NULLIF(TRIM(`device`), ''), '未知') AS device, COUNT(*) AS cnt
|
||||
FROM `visitor_logs` WHERE {$where}
|
||||
GROUP BY device ORDER BY cnt DESC LIMIT {$limit}";
|
||||
$res = $conn->query($sql);
|
||||
$rows = [];
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$rows[] = [
|
||||
'device' => (string) $r['device'],
|
||||
'count' => (int) $r['cnt'],
|
||||
];
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $where
|
||||
*/
|
||||
private static function fetchTimingStats($conn, $where)
|
||||
{
|
||||
$sql = "SELECT `judge_timing` FROM `visitor_logs`
|
||||
WHERE {$where} AND `judge_timing` IS NOT NULL AND `judge_timing` <> ''
|
||||
ORDER BY `id` DESC LIMIT 500";
|
||||
$res = $conn->query($sql);
|
||||
|
||||
$stageTotals = [];
|
||||
$stageCounts = [];
|
||||
$totalMsSum = 0;
|
||||
$totalMsCnt = 0;
|
||||
|
||||
if ($res) {
|
||||
while ($r = $res->fetch_assoc()) {
|
||||
$decoded = json_decode((string) ($r['judge_timing'] ?? ''), true);
|
||||
if (!is_array($decoded)) {
|
||||
continue;
|
||||
}
|
||||
if (isset($decoded['total_ms'])) {
|
||||
$totalMsSum += (float) $decoded['total_ms'];
|
||||
$totalMsCnt++;
|
||||
}
|
||||
if (!empty($decoded['stages']) && is_array($decoded['stages'])) {
|
||||
foreach ($decoded['stages'] as $stage) {
|
||||
if (!is_array($stage)) {
|
||||
continue;
|
||||
}
|
||||
$name = trim((string) ($stage['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
continue;
|
||||
}
|
||||
$ms = (float) ($stage['ms'] ?? 0);
|
||||
if (!isset($stageTotals[$name])) {
|
||||
$stageTotals[$name] = 0;
|
||||
$stageCounts[$name] = 0;
|
||||
}
|
||||
$stageTotals[$name] += $ms;
|
||||
$stageCounts[$name]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
|
||||
$stages = [];
|
||||
foreach ($stageTotals as $name => $sum) {
|
||||
$cnt = max(1, $stageCounts[$name]);
|
||||
$stages[] = [
|
||||
'stage' => $name,
|
||||
'avg_ms' => round($sum / $cnt, 2),
|
||||
];
|
||||
}
|
||||
usort($stages, static function ($a, $b) {
|
||||
return $b['avg_ms'] <=> $a['avg_ms'];
|
||||
});
|
||||
|
||||
return [
|
||||
'avg_total_ms' => $totalMsCnt > 0 ? round($totalMsSum / $totalMsCnt, 2) : 0,
|
||||
'sample_size' => $totalMsCnt,
|
||||
'stages' => $stages,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param array $filters
|
||||
* @param array $currentSummary
|
||||
*/
|
||||
private static function fetchCompare($conn, array $filters, array $currentSummary)
|
||||
{
|
||||
$dateFrom = trim((string) ($filters['date_from'] ?? ''));
|
||||
$dateTo = trim((string) ($filters['date_to'] ?? ''));
|
||||
if ($dateFrom === '' || $dateTo === '') {
|
||||
$dateTo = date('Y-m-d');
|
||||
$dateFrom = date('Y-m-d', strtotime('-6 days'));
|
||||
}
|
||||
|
||||
$start = strtotime($dateFrom . ' 00:00:00');
|
||||
$end = strtotime($dateTo . ' 23:59:59');
|
||||
if ($start === false || $end === false || $end < $start) {
|
||||
return [
|
||||
'prev_total' => 0,
|
||||
'pass_rate_delta' => 0,
|
||||
'block_rate_delta' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$days = max(1, (int) floor(($end - $start) / 86400) + 1);
|
||||
$prevEnd = date('Y-m-d 23:59:59', $start - 86400);
|
||||
$prevStart = date('Y-m-d 00:00:00', $start - ($days * 86400));
|
||||
|
||||
$prevFilters = $filters;
|
||||
$prevFilters['date_from'] = substr($prevStart, 0, 10);
|
||||
$prevFilters['date_to'] = substr($prevEnd, 0, 10);
|
||||
$prevWhere = self::buildWhere($conn, $prevFilters);
|
||||
$prevSummary = self::fetchSummary($conn, $prevWhere);
|
||||
|
||||
return [
|
||||
'prev_total' => (int) $prevSummary['total'],
|
||||
'pass_rate_delta' => round($currentSummary['pass_rate'] - $prevSummary['pass_rate'], 1),
|
||||
'block_rate_delta' => round($currentSummary['block_rate'] - $prevSummary['block_rate'], 1),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $summary
|
||||
* @param array $byReason
|
||||
* @param array $byCountry
|
||||
* @param array $byHour
|
||||
* @param array $byDevice
|
||||
* @param array $timing
|
||||
* @param array $compare
|
||||
* @param array $byResult
|
||||
* @return string[]
|
||||
*/
|
||||
private static function buildConclusions(array $summary, array $byReason, array $byCountry, array $byHour, array $byDevice, array $timing, array $compare, array $byResult)
|
||||
{
|
||||
$lines = [];
|
||||
$total = (int) $summary['total'];
|
||||
|
||||
if ($total === 0) {
|
||||
$lines[] = '所选时间范围内暂无访客日志,请调整筛选条件或确认异步队列已落库。';
|
||||
return $lines;
|
||||
}
|
||||
|
||||
$lines[] = sprintf(
|
||||
'共记录 %d 次访问,通过率 %.1f%%,拦截率 %.1f%%,检测中占比 %.1f%%。',
|
||||
$total,
|
||||
$summary['pass_rate'],
|
||||
$summary['block_rate'],
|
||||
$summary['wait_rate']
|
||||
);
|
||||
|
||||
if ($compare['prev_total'] > 0) {
|
||||
$deltaSign = $compare['pass_rate_delta'] >= 0 ? '上升' : '下降';
|
||||
$lines[] = sprintf(
|
||||
'与上一同等周期(%d 条)相比,通过率%s %.1f 个百分点,拦截率变化 %.1f 个百分点。',
|
||||
$compare['prev_total'],
|
||||
$deltaSign,
|
||||
abs($compare['pass_rate_delta']),
|
||||
$compare['block_rate_delta']
|
||||
);
|
||||
}
|
||||
|
||||
if ($byReason !== []) {
|
||||
$top = $byReason[0];
|
||||
$lines[] = sprintf(
|
||||
'主要拦截/待判定理由为「%s」,占该类记录 %.1f%%(%d 次)。',
|
||||
$top['reason'],
|
||||
$top['pct'],
|
||||
$top['count']
|
||||
);
|
||||
if (count($byReason) >= 2) {
|
||||
$second = $byReason[1];
|
||||
$lines[] = sprintf('次常见理由:「%s」(%d 次,%.1f%%)。', $second['reason'], $second['count'], $second['pct']);
|
||||
}
|
||||
}
|
||||
|
||||
$peakHour = null;
|
||||
$peakCnt = 0;
|
||||
foreach ($byHour as $h) {
|
||||
if ($h['count'] > $peakCnt) {
|
||||
$peakCnt = $h['count'];
|
||||
$peakHour = $h['hour'];
|
||||
}
|
||||
}
|
||||
if ($peakHour !== null && $peakCnt > 0) {
|
||||
$lines[] = sprintf('访问高峰时段为 %s,共 %d 次访问。', $peakHour, $peakCnt);
|
||||
}
|
||||
|
||||
if ($byCountry !== []) {
|
||||
$topC = $byCountry[0];
|
||||
$countryLabel = $topC['country'] === '(空)' ? '未知国家' : $topC['country'];
|
||||
$lines[] = sprintf('访客来源以「%s」最多,共 %d 次。', $countryLabel, $topC['count']);
|
||||
}
|
||||
|
||||
if ($byDevice !== []) {
|
||||
$lines[] = sprintf('设备分布首位:%s(%d 次)。', $byDevice[0]['device'], $byDevice[0]['count']);
|
||||
}
|
||||
|
||||
if ($timing['sample_size'] > 0) {
|
||||
$lines[] = sprintf('判定耗时抽样 %d 条,平均总耗时 %.2f ms。', $timing['sample_size'], $timing['avg_total_ms']);
|
||||
if (!empty($timing['stages'])) {
|
||||
$slow = $timing['stages'][0];
|
||||
$lines[] = sprintf('最慢阶段为「%s」,平均 %.2f ms。', $slow['stage'], $slow['avg_ms']);
|
||||
}
|
||||
}
|
||||
|
||||
if ($summary['block_rate'] > 60) {
|
||||
$lines[] = '告警:拦截率超过 60%,建议检查 API 配置、黑白名单及广告来源规则是否过严。';
|
||||
}
|
||||
if ($summary['wait_rate'] > 5) {
|
||||
$lines[] = '告警:检测中(wait)状态占比偏高,可能存在二次风控未完成或 f_check 回调异常。';
|
||||
}
|
||||
|
||||
foreach (array_slice($byReason, 0, 3) as $r) {
|
||||
if ($r['pct'] >= 40 && $total >= 20) {
|
||||
$lines[] = sprintf('关注:理由「%s」占比达 %.1f%%,建议针对性排查该规则。', $r['reason'], $r['pct']);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $lines;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
/**
|
||||
* 访客日志异步写入队列(Redis 优先,文件降级)
|
||||
*/
|
||||
class VisitorLogQueue
|
||||
{
|
||||
const REDIS_KEY = 'cloak:visitor_log_queue';
|
||||
const FILE_DIR = 'visitor_log_queue';
|
||||
|
||||
/** @var bool|null */
|
||||
private static $redisAvailable = null;
|
||||
|
||||
/**
|
||||
* @param array $job 需含 type: insert_full|enrich
|
||||
* @return bool
|
||||
*/
|
||||
public static function push(array $job)
|
||||
{
|
||||
$job['queued_at'] = date('Y-m-d H:i:s');
|
||||
$payload = json_encode($job, JSON_UNESCAPED_UNICODE);
|
||||
if ($payload === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$redis = self::redisClient();
|
||||
if ($redis) {
|
||||
try {
|
||||
return (bool) $redis->lPush(self::REDIS_KEY, $payload);
|
||||
} catch (Throwable $e) {
|
||||
self::logError('Redis push failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return self::pushFile($payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array>
|
||||
*/
|
||||
public static function popBatch(int $limit = 100)
|
||||
{
|
||||
$limit = max(1, min(500, $limit));
|
||||
$jobs = [];
|
||||
|
||||
$redis = self::redisClient();
|
||||
if ($redis) {
|
||||
try {
|
||||
for ($i = 0; $i < $limit; $i++) {
|
||||
$raw = $redis->rPop(self::REDIS_KEY);
|
||||
if ($raw === false || $raw === null) {
|
||||
break;
|
||||
}
|
||||
$decoded = json_decode((string) $raw, true);
|
||||
if (is_array($decoded)) {
|
||||
$jobs[] = $decoded;
|
||||
}
|
||||
}
|
||||
if ($jobs !== []) {
|
||||
return $jobs;
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
self::logError('Redis pop failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return self::popFileBatch($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求 shutdown 时轻量 drain(最多 $limit 条)
|
||||
*/
|
||||
public static function drain(int $limit = 5)
|
||||
{
|
||||
if (!class_exists('VisitorLogWorker', false)) {
|
||||
require_once __DIR__ . '/VisitorLogWorker.php';
|
||||
}
|
||||
VisitorLogWorker::processBatch($limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Redis|null
|
||||
*/
|
||||
private static function redisClient()
|
||||
{
|
||||
if (self::$redisAvailable === false) {
|
||||
return null;
|
||||
}
|
||||
if (!extension_loaded('redis') || !defined('REDIS_ENABLED') || strtoupper(REDIS_ENABLED) !== 'ON') {
|
||||
self::$redisAvailable = false;
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$r = new Redis();
|
||||
$timeout = defined('REDIS_TIMEOUT') ? (float) REDIS_TIMEOUT : 1.0;
|
||||
if (!@$r->connect(REDIS_HOST, (int) REDIS_PORT, $timeout)) {
|
||||
self::$redisAvailable = false;
|
||||
return null;
|
||||
}
|
||||
if (defined('REDIS_PASSWORD') && REDIS_PASSWORD !== '') {
|
||||
$r->auth(REDIS_PASSWORD);
|
||||
}
|
||||
self::$redisAvailable = true;
|
||||
return $r;
|
||||
} catch (Throwable $e) {
|
||||
self::$redisAvailable = false;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static function queueDir()
|
||||
{
|
||||
$dir = dirname(__DIR__, 2) . '/storage/' . self::FILE_DIR;
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
return $dir;
|
||||
}
|
||||
|
||||
private static function pushFile($payload)
|
||||
{
|
||||
$dir = self::queueDir();
|
||||
$file = $dir . '/pending.ndjson';
|
||||
$fp = @fopen($file, 'ab');
|
||||
if (!$fp) {
|
||||
return false;
|
||||
}
|
||||
if (flock($fp, LOCK_EX)) {
|
||||
fwrite($fp, $payload . "\n");
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
}
|
||||
fclose($fp);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array>
|
||||
*/
|
||||
private static function popFileBatch(int $limit)
|
||||
{
|
||||
$dir = self::queueDir();
|
||||
$file = $dir . '/pending.ndjson';
|
||||
if (!is_file($file)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$fp = @fopen($file, 'c+');
|
||||
if (!$fp) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$jobs = [];
|
||||
if (!flock($fp, LOCK_EX)) {
|
||||
fclose($fp);
|
||||
return [];
|
||||
}
|
||||
|
||||
$lines = [];
|
||||
while (($line = fgets($fp)) !== false) {
|
||||
$line = trim($line);
|
||||
if ($line !== '') {
|
||||
$lines[] = $line;
|
||||
}
|
||||
}
|
||||
|
||||
if ($lines === []) {
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
return [];
|
||||
}
|
||||
|
||||
$take = array_slice($lines, 0, $limit);
|
||||
$remain = array_slice($lines, $limit);
|
||||
ftruncate($fp, 0);
|
||||
rewind($fp);
|
||||
foreach ($remain as $line) {
|
||||
fwrite($fp, $line . "\n");
|
||||
}
|
||||
fflush($fp);
|
||||
flock($fp, LOCK_UN);
|
||||
fclose($fp);
|
||||
|
||||
foreach ($take as $line) {
|
||||
$decoded = json_decode($line, true);
|
||||
if (is_array($decoded)) {
|
||||
$jobs[] = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
return $jobs;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $job
|
||||
* @param string $error
|
||||
*/
|
||||
public static function moveToDead(array $job, $error)
|
||||
{
|
||||
$dir = self::queueDir() . '/dead';
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0755, true);
|
||||
}
|
||||
$job['dead_at'] = date('Y-m-d H:i:s');
|
||||
$job['dead_error'] = (string) $error;
|
||||
$name = $dir . '/' . date('Ymd_His') . '_' . substr(md5(json_encode($job)), 0, 8) . '.json';
|
||||
@file_put_contents($name, json_encode($job, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||
self::logError('Dead letter: ' . $error);
|
||||
}
|
||||
|
||||
public static function logError($message)
|
||||
{
|
||||
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
if ($handle) {
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | VisitorLogQueue | ' . $message . "\n");
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+92
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* visitor_logs 表结构升级(新增请求头与判定时长字段)
|
||||
*/
|
||||
class VisitorLogSchema
|
||||
{
|
||||
/** @var bool */
|
||||
private static $ensured = false;
|
||||
|
||||
/** @var bool */
|
||||
private static $indexesEnsured = false;
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
*/
|
||||
public static function ensureColumns($conn)
|
||||
{
|
||||
if (self::$ensured) {
|
||||
return;
|
||||
}
|
||||
$columns = [
|
||||
'user_agent' => "ALTER TABLE `visitor_logs` ADD COLUMN `user_agent` TEXT NULL COMMENT 'User-Agent 完整值' AFTER `language`",
|
||||
'http_referer' => "ALTER TABLE `visitor_logs` ADD COLUMN `http_referer` TEXT NULL COMMENT 'HTTP Referer 完整值' AFTER `user_agent`",
|
||||
'accept_language_raw' => "ALTER TABLE `visitor_logs` ADD COLUMN `accept_language_raw` TEXT NULL COMMENT 'Accept-Language 完整值' AFTER `http_referer`",
|
||||
'judge_timing' => "ALTER TABLE `visitor_logs` ADD COLUMN `judge_timing` TEXT NULL COMMENT '判定用时 JSON' AFTER `accept_language_raw`",
|
||||
];
|
||||
foreach ($columns as $name => $ddl) {
|
||||
if (!self::columnExists($conn, 'visitor_logs', $name)) {
|
||||
@$conn->query($ddl);
|
||||
}
|
||||
}
|
||||
self::$ensured = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
*/
|
||||
public static function ensureIndexes($conn)
|
||||
{
|
||||
if (self::$indexesEnsured) {
|
||||
return;
|
||||
}
|
||||
$indexes = [
|
||||
'idx_visit_date' => 'ALTER TABLE `visitor_logs` ADD INDEX `idx_visit_date` (`visit_date`)',
|
||||
'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`)',
|
||||
];
|
||||
foreach ($indexes as $name => $ddl) {
|
||||
if (!self::indexExists($conn, 'visitor_logs', $name)) {
|
||||
@$conn->query($ddl);
|
||||
}
|
||||
}
|
||||
self::$indexesEnsured = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $table
|
||||
* @param string $indexName
|
||||
*/
|
||||
private static function indexExists($conn, $table, $indexName)
|
||||
{
|
||||
$tableEsc = cloak_db_escape($conn, $table);
|
||||
$indexEsc = cloak_db_escape($conn, $indexName);
|
||||
$sql = "SHOW INDEX FROM `{$tableEsc}` WHERE Key_name = '{$indexEsc}'";
|
||||
if ($res = $conn->query($sql)) {
|
||||
$exists = $res->num_rows > 0;
|
||||
$res->free();
|
||||
return $exists;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param string $table
|
||||
* @param string $column
|
||||
*/
|
||||
private static function columnExists($conn, $table, $column)
|
||||
{
|
||||
$tableEsc = cloak_db_escape($conn, $table);
|
||||
$columnEsc = cloak_db_escape($conn, $column);
|
||||
$sql = "SHOW COLUMNS FROM `{$tableEsc}` LIKE '{$columnEsc}'";
|
||||
if ($res = $conn->query($sql)) {
|
||||
$exists = $res->num_rows > 0;
|
||||
$res->free();
|
||||
return $exists;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* 访客日志队列消费(INSERT / enrich UPDATE)
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/DbHelper.php';
|
||||
require_once __DIR__ . '/VisitorLogSchema.php';
|
||||
require_once __DIR__ . '/VisitorLogQueue.php';
|
||||
|
||||
class VisitorLogWorker
|
||||
{
|
||||
/**
|
||||
* @return array{processed:int, failed:int}
|
||||
*/
|
||||
public static function processBatch(int $limit = 100)
|
||||
{
|
||||
$jobs = VisitorLogQueue::popBatch($limit);
|
||||
$processed = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
$ok = false;
|
||||
for ($attempt = 1; $attempt <= 3; $attempt++) {
|
||||
if (self::processJob($job)) {
|
||||
$ok = true;
|
||||
break;
|
||||
}
|
||||
usleep(100000 * $attempt);
|
||||
}
|
||||
if ($ok) {
|
||||
$processed++;
|
||||
} else {
|
||||
$failed++;
|
||||
VisitorLogQueue::moveToDead($job, 'process failed after retries');
|
||||
}
|
||||
}
|
||||
|
||||
return ['processed' => $processed, 'failed' => $failed];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $job
|
||||
*/
|
||||
public static function processJob(array $job)
|
||||
{
|
||||
$type = $job['type'] ?? '';
|
||||
if ($type === 'insert_full') {
|
||||
return self::insertFull($job['logs'] ?? []) !== null;
|
||||
}
|
||||
if ($type === 'enrich') {
|
||||
return self::enrich((int) ($job['log_id'] ?? 0), $job['logs'] ?? []);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $logs
|
||||
* @return int|null insert_id
|
||||
*/
|
||||
public static function insertFull(array $logs)
|
||||
{
|
||||
$conn = self::connect();
|
||||
if (!$conn) {
|
||||
return null;
|
||||
}
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
VisitorLogSchema::ensureIndexes($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, '');
|
||||
};
|
||||
|
||||
$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`) VALUES ('"
|
||||
. $d('campagin_id') . "','"
|
||||
. $d('visit_md5_code') . "','"
|
||||
. $d('site_name') . "','"
|
||||
. ($data['visit_date'] ?? date('Y-m-d H:i:s')) . "','"
|
||||
. $d('customers_ip') . "','"
|
||||
. $d('country') . "','"
|
||||
. $d('result') . "','"
|
||||
. $d('reason') . "','"
|
||||
. $d('v_referer') . "','"
|
||||
. $d('Client') . "','"
|
||||
. $d('v_Browser') . "','"
|
||||
. $d('v_PageURL') . "','"
|
||||
. $d('accept_language') . "','"
|
||||
. $d('user_agent') . "','"
|
||||
. $d('http_referer') . "','"
|
||||
. $d('accept_language_raw') . "','"
|
||||
. $d('judge_timing') . "','"
|
||||
. $d('fp_url') . "','"
|
||||
. $d('device') . "')";
|
||||
|
||||
if ($conn->query($sql) !== true) {
|
||||
VisitorLogQueue::logError('insert_full: ' . $conn->error . ' | ' . $sql);
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
$id = (int) $conn->insert_id;
|
||||
$conn->close();
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步最小 INSERT(二次风控 wait,需立即返回 log_id)
|
||||
*
|
||||
* @param array $logs
|
||||
* @return int|null
|
||||
*/
|
||||
public static function insertMinimal(array $logs)
|
||||
{
|
||||
$conn = self::connect();
|
||||
if (!$conn) {
|
||||
return null;
|
||||
}
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
VisitorLogSchema::ensureIndexes($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, '');
|
||||
};
|
||||
|
||||
$sql = "INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `client`, `browser`, `language`, `fp_url`, `device`) VALUES ('"
|
||||
. $d('campagin_id') . "','"
|
||||
. $d('visit_md5_code') . "','"
|
||||
. $d('site_name') . "','"
|
||||
. date('Y-m-d H:i:s') . "','"
|
||||
. $d('customers_ip') . "','"
|
||||
. $d('country') . "','"
|
||||
. $d('result') . "','"
|
||||
. $d('reason') . "','"
|
||||
. $d('Client') . "','"
|
||||
. $d('v_Browser') . "','"
|
||||
. $d('accept_language') . "','"
|
||||
. $d('fp_url') . "','"
|
||||
. $d('device') . "')";
|
||||
|
||||
if ($conn->query($sql) !== true) {
|
||||
VisitorLogQueue::logError('insertMinimal: ' . $conn->error);
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
$id = (int) $conn->insert_id;
|
||||
$conn->close();
|
||||
return $id > 0 ? $id : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $logId
|
||||
* @param array $logs
|
||||
*/
|
||||
public static function enrich($logId, array $logs)
|
||||
{
|
||||
if ($logId <= 0) {
|
||||
return false;
|
||||
}
|
||||
$conn = self::connect();
|
||||
if (!$conn) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data = self::escapeLogs($conn, $logs);
|
||||
$sets = [];
|
||||
$map = [
|
||||
'referer' => 'v_referer',
|
||||
'page' => 'v_PageURL',
|
||||
'user_agent' => 'user_agent',
|
||||
'http_referer' => 'http_referer',
|
||||
'accept_language_raw' => 'accept_language_raw',
|
||||
'judge_timing' => 'judge_timing',
|
||||
];
|
||||
foreach ($map as $col => $logKey) {
|
||||
if (array_key_exists($logKey, $data)) {
|
||||
$sets[] = "`{$col}`='" . $data[$logKey] . "'";
|
||||
} elseif (array_key_exists($col, $data)) {
|
||||
$sets[] = "`{$col}`='" . $data[$col] . "'";
|
||||
}
|
||||
}
|
||||
if ($sets === []) {
|
||||
$conn->close();
|
||||
return true;
|
||||
}
|
||||
|
||||
$sql = 'UPDATE `visitor_logs` SET ' . implode(', ', $sets) . ' WHERE `id`=' . (int) $logId;
|
||||
$ok = $conn->query($sql) === true;
|
||||
if (!$ok) {
|
||||
VisitorLogQueue::logError('enrich: ' . $conn->error);
|
||||
}
|
||||
$conn->close();
|
||||
return $ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mysqli $conn
|
||||
* @param array $logs
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private static function escapeLogs($conn, array $logs)
|
||||
{
|
||||
$data = [];
|
||||
foreach ($logs as $key => $value) {
|
||||
$data[$key] = cloak_db_escape($conn, $value);
|
||||
}
|
||||
if (!isset($data['visit_date'])) {
|
||||
$data['visit_date'] = date('Y-m-d H:i:s');
|
||||
}
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mysqli|null
|
||||
*/
|
||||
private static function connect()
|
||||
{
|
||||
if (!defined('DB_USERNAME') || !defined('DB_PASSWORD') || !defined('DB_NAME')) {
|
||||
return null;
|
||||
}
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
VisitorLogQueue::logError('DB connect: ' . $conn->connect_error);
|
||||
return null;
|
||||
}
|
||||
return $conn;
|
||||
}
|
||||
}
|
||||
Executable
+355
@@ -0,0 +1,355 @@
|
||||
<?php
|
||||
/**
|
||||
* 访客日志与 IP 名单数据访问
|
||||
*/
|
||||
require_once dirname(__DIR__) . '/DbHelper.php';
|
||||
require_once dirname(__DIR__) . '/CloakHttpHelpers.php';
|
||||
|
||||
if (!function_exists('cloak_session_mark_real_visitor')) {
|
||||
/**
|
||||
* 后台「真实访客」:写入完整判定缓存,访问落地链接时走 Session 快速路径并直达真实页
|
||||
*/
|
||||
function cloak_session_mark_real_visitor()
|
||||
{
|
||||
$gcuid = !empty($_COOKIE['gfuid']) ? (string) $_COOKIE['gfuid'] : '132456789';
|
||||
|
||||
$_SESSION['check_result'] = 'true';
|
||||
$_SESSION['visit_to_1'] = '1';
|
||||
$_SESSION['visit_to_2'] = '2';
|
||||
$_SESSION['visit_to_3'] = '3';
|
||||
$_SESSION['gcuid'] = $gcuid;
|
||||
$_SESSION['gcu_ip'] = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
|
||||
if (empty($_SESSION['gcu_country']) && defined('SHOW_SITE_COUNTRY')) {
|
||||
$_SESSION['gcu_country'] = SHOW_SITE_COUNTRY;
|
||||
}
|
||||
|
||||
setCrossDomainCookie('gfuid', $gcuid, time() + 3600 * 24);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_session_clear_real_visitor')) {
|
||||
function cloak_session_clear_real_visitor()
|
||||
{
|
||||
unset(
|
||||
$_SESSION['check_result'],
|
||||
$_SESSION['visit_to_1'],
|
||||
$_SESSION['visit_to_2'],
|
||||
$_SESSION['visit_to_3'],
|
||||
$_SESSION['gcuid'],
|
||||
$_SESSION['gcu_ip'],
|
||||
$_SESSION['gcu_country'],
|
||||
$_SESSION['gcu_Client'],
|
||||
$_SESSION['gcu_Browser'],
|
||||
$_SESSION['gcu_referer']
|
||||
);
|
||||
setCrossDomainCookie('gfuid', '', time() - 3600);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ip_group_list_addresses')) {
|
||||
function ip_group_list_addresses($groupId)
|
||||
{
|
||||
$groupId = (int) $groupId;
|
||||
if ($groupId <= 0) {
|
||||
return [];
|
||||
}
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
return [];
|
||||
}
|
||||
$out = [];
|
||||
$sql = 'SELECT ip_address FROM ip_list WHERE group_id = ' . $groupId;
|
||||
if ($res = $conn->query($sql)) {
|
||||
while ($row = $res->fetch_assoc()) {
|
||||
$out[] = $row['ip_address'];
|
||||
}
|
||||
$res->free();
|
||||
}
|
||||
$conn->close();
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ip_visitor_matches_list_entry')) {
|
||||
function ip_visitor_matches_list_entry($visitorIp, $entry)
|
||||
{
|
||||
$entry = trim($entry);
|
||||
if ($entry === '') {
|
||||
return false;
|
||||
}
|
||||
if ($visitorIp === $entry) {
|
||||
return true;
|
||||
}
|
||||
if (mb_substr($entry, -1) === '*') {
|
||||
return strpos($visitorIp, rtrim($entry, '*')) === 0;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('ip_visitor_in_blacklist_entries')) {
|
||||
function ip_visitor_in_blacklist_entries($visitorIp, array $entries)
|
||||
{
|
||||
foreach ($entries as $buffer) {
|
||||
if (ip_visitor_matches_list_entry($visitorIp, $buffer)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_visitor_log_request_meta')) {
|
||||
function cloak_visitor_log_request_meta()
|
||||
{
|
||||
return [
|
||||
'user_agent' => get_SERVER_value('HTTP_USER_AGENT'),
|
||||
'http_referer' => get_SERVER_value('HTTP_REFERER'),
|
||||
'accept_language_raw' => get_SERVER_value('HTTP_ACCEPT_LANGUAGE'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_visitor_log_enrich')) {
|
||||
function cloak_visitor_log_enrich(array $logs)
|
||||
{
|
||||
$meta = cloak_visitor_log_request_meta();
|
||||
foreach ($meta as $key => $value) {
|
||||
if (!array_key_exists($key, $logs) || $logs[$key] === null || $logs[$key] === '') {
|
||||
$logs[$key] = $value;
|
||||
}
|
||||
}
|
||||
if (empty($logs['judge_timing']) && !empty($GLOBALS['__cloak_judge_timing'])) {
|
||||
$logs['judge_timing'] = $GLOBALS['__cloak_judge_timing'];
|
||||
}
|
||||
return $logs;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_visitor_log_async_enabled')) {
|
||||
function cloak_visitor_log_async_enabled()
|
||||
{
|
||||
return defined('VISITOR_LOG_ASYNC') && strtoupper(VISITOR_LOG_ASYNC) === 'ON';
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_visitor_log_register_shutdown_drain')) {
|
||||
function cloak_visitor_log_register_shutdown_drain()
|
||||
{
|
||||
static $registered = false;
|
||||
if ($registered || !cloak_visitor_log_async_enabled()) {
|
||||
return;
|
||||
}
|
||||
$registered = true;
|
||||
register_shutdown_function(static function () {
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
@fastcgi_finish_request();
|
||||
}
|
||||
VisitorLogQueue::drain(5);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_write_log_db_sync')) {
|
||||
function cloak_write_log_db_sync($logs)
|
||||
{
|
||||
return VisitorLogWorker::insertFull($logs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_write_log_db')) {
|
||||
function cloak_write_log_db($logs)
|
||||
{
|
||||
$logs = cloak_visitor_log_enrich($logs);
|
||||
|
||||
if (defined('WRITE_LOG') && WRITE_LOG == '1') {
|
||||
$my_url = $_SERVER['PHP_SELF'] ?? '';
|
||||
$my_file_name = substr($my_url, strrpos($my_url, '/') + 1);
|
||||
$config_name = str_replace('.php', '', $my_file_name);
|
||||
$fp = fopen(dirname(__DIR__, 2) . '/jump.txt', 'a+');
|
||||
fwrite($fp, date('Y-m-d H:i:s') . ' | 写入数据库 ' . ' | ' . ($_SESSION['check_result'] ?? '') . ' | ' . $config_name . ' | async=' . (cloak_visitor_log_async_enabled() ? '1' : '0') . "\n");
|
||||
fclose($fp);
|
||||
}
|
||||
|
||||
$result = isset($logs['result']) ? (string) $logs['result'] : '';
|
||||
|
||||
// 二次风控 wait:同步最小 INSERT 获取 log_id,TEXT 字段异步 enrich
|
||||
if ($result === 'wait') {
|
||||
$logId = VisitorLogWorker::insertMinimal($logs);
|
||||
if ($logId === null) {
|
||||
return cloak_write_log_db_sync($logs);
|
||||
}
|
||||
if (cloak_visitor_log_async_enabled()) {
|
||||
VisitorLogQueue::push([
|
||||
'type' => 'enrich',
|
||||
'log_id' => $logId,
|
||||
'logs' => $logs,
|
||||
]);
|
||||
} else {
|
||||
VisitorLogWorker::enrich($logId, $logs);
|
||||
}
|
||||
return $logId;
|
||||
}
|
||||
|
||||
if (!cloak_visitor_log_async_enabled()) {
|
||||
$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
$error = 'Connection failed: ' . $conn->connect_error;
|
||||
$handle = fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | ' . $error . "\n");
|
||||
fclose($handle);
|
||||
return null;
|
||||
}
|
||||
VisitorLogSchema::ensureColumns($conn);
|
||||
$data = [];
|
||||
foreach ($logs as $key => $value) {
|
||||
$data[$key] = cloak_db_escape($conn, $value);
|
||||
}
|
||||
$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`) VALUES ('"
|
||||
. $d('campagin_id') . "','"
|
||||
. $d('visit_md5_code') . "','"
|
||||
. $d('site_name') . "','"
|
||||
. date('Y-m-d H:i:s') . "','"
|
||||
. $d('customers_ip') . "','"
|
||||
. $d('country') . "','"
|
||||
. $d('result') . "','"
|
||||
. $d('reason') . "','"
|
||||
. $d('v_referer') . "','"
|
||||
. $d('Client') . "','"
|
||||
. $d('v_Browser') . "','"
|
||||
. $d('v_PageURL') . "','"
|
||||
. $d('accept_language') . "','"
|
||||
. $d('user_agent') . "','"
|
||||
. $d('http_referer') . "','"
|
||||
. $d('accept_language_raw') . "','"
|
||||
. $d('judge_timing') . "','"
|
||||
. $d('fp_url') . "','"
|
||||
. $d('device') . "')";
|
||||
if ($conn->query($sql) === true) {
|
||||
$last_id = $conn->insert_id;
|
||||
$conn->close();
|
||||
return $last_id;
|
||||
}
|
||||
$handle = fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | ' . 'Error: ' . $sql . '<br>' . $conn->error . "\n");
|
||||
fclose($handle);
|
||||
$conn->close();
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!VisitorLogQueue::push(['type' => 'insert_full', 'logs' => $logs])) {
|
||||
return cloak_write_log_db_sync($logs);
|
||||
}
|
||||
cloak_visitor_log_register_shutdown_drain();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('write_log_db')) {
|
||||
function write_log_db($logs)
|
||||
{
|
||||
return cloak_write_log_db($logs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_howmanytimesbyip')) {
|
||||
function cloak_howmanytimesbyip($ip)
|
||||
{
|
||||
$row = ['total' => 0];
|
||||
$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
$handle = fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | Connection failed: ' . $conn->connect_error . "\n");
|
||||
fclose($handle);
|
||||
return 0;
|
||||
}
|
||||
$ipEsc = cloak_db_escape($conn, $ip);
|
||||
$sql = "SELECT count(*) as `total` FROM `visitor_logs` WHERE `IP`='" . $ipEsc . "'";
|
||||
if ($conn->query($sql) == true) {
|
||||
$result = $conn->query($sql);
|
||||
$row = $result->fetch_array();
|
||||
$conn->close();
|
||||
} else {
|
||||
$handle = fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
|
||||
fwrite($handle, date('Y-m-d H:i:s') . ' | Error: ' . $sql . '<br>' . $conn->error . "\n");
|
||||
fclose($handle);
|
||||
$conn->close();
|
||||
return 0;
|
||||
}
|
||||
return $row['total'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('howmanytimesbyip')) {
|
||||
function howmanytimesbyip($ip)
|
||||
{
|
||||
return cloak_howmanytimesbyip($ip);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('cloak_write_black_list')) {
|
||||
function cloak_write_black_list($ip)
|
||||
{
|
||||
if (!defined('BLACKLIST_GROUP_ID') || (int) BLACKLIST_GROUP_ID <= 0) {
|
||||
return;
|
||||
}
|
||||
$gid = (int) BLACKLIST_GROUP_ID;
|
||||
$conn = @new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
|
||||
if ($conn->connect_error) {
|
||||
return;
|
||||
}
|
||||
$stmt = $conn->prepare('INSERT IGNORE INTO ip_list (group_id, ip_address) VALUES (?, ?)');
|
||||
if ($stmt) {
|
||||
$stmt->bind_param('is', $gid, $ip);
|
||||
$stmt->execute();
|
||||
$stmt->close();
|
||||
}
|
||||
$conn->close();
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('write_black_list')) {
|
||||
function write_black_list($ip)
|
||||
{
|
||||
cloak_write_black_list($ip);
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('browser_headers')) {
|
||||
function browser_headers()
|
||||
{
|
||||
$headers = [];
|
||||
foreach ($_SERVER as $name => $value) {
|
||||
if (preg_match('/^HTTP_/', $name)) {
|
||||
$name = strtolower(strtr(substr($name, 5), '_', '-'));
|
||||
$headers[$name] = $value;
|
||||
}
|
||||
}
|
||||
return $headers;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('accept_lang_zh')) {
|
||||
function accept_lang_zh()
|
||||
{
|
||||
if (empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
|
||||
return false;
|
||||
}
|
||||
$lang = substr((string) $_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 4);
|
||||
if (preg_match('/zh-c/i', $lang) or preg_match('/zh/i', $lang)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('is_https')) {
|
||||
function is_https()
|
||||
{
|
||||
return cloak_request_is_https();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user