feat: initial commit

This commit is contained in:
root
2026-06-14 14:00:24 +08:00
commit 72d388f642
118 changed files with 39015 additions and 0 deletions
Executable
+5
View File
@@ -0,0 +1,5 @@
# 忽略 Cursor 和 VS Code 的编辑器缓存
.sidebar/
.cursor/
.vscode/
*.log
Executable
+24
View File
@@ -0,0 +1,24 @@
<IfModule mod_rewrite.c>
# 开启重写引擎
RewriteEngine On
# 对应:index index.html index.htm index.php;
DirectoryIndex index.html index.htm index.php
# 1. 如果请求的是真实存在的目录或文件,直接跳过重写
RewriteCond %{REQUEST_FILENAME} -d [OR]
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]
# 2. 对应:location ~ \.php$ { try_files $uri =404; }
# 保护机制:如果请求以 .php 结尾但文件不存在,直接返回 404 (这也能防止下方的规则产生死循环)
RewriteCond %{REQUEST_URI} \.php$
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ - [R=404,L]
# 3. 对应:try_files $uri $uri/ @extensionless-php; 以及 rewrite ^(.*)$ $1.php last;
# 将所有找不到的请求追加 .php 后缀进行内部重定向
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ $1.php [L]
</IfModule>
Executable
+1
View File
@@ -0,0 +1 @@
2026-05-24 21:55:43 installed
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Şerban Ghiţă, Nick Ilyin and contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+233
View File
File diff suppressed because one or more lines are too long
+321
View File
@@ -0,0 +1,321 @@
![Mobile Detect](http://demo.mobiledetect.net/logo-github.png)
# MobileDetect, PHP mobile detection class
![Workflow status](https://img.shields.io/github/actions/workflow/status/serbanghita/Mobile-Detect/3.74.x-test.yml?style=flat-square)
![Latest tag](https://img.shields.io/github/v/tag/serbanghita/Mobile-Detect?filter=3.*&style=flat-square)
![Monthly Downloads](https://img.shields.io/packagist/dm/mobiledetect/mobiledetectlib?style=flat-square&label=installs)
![Total Downloads](https://img.shields.io/packagist/dt/mobiledetect/mobiledetectlib?style=flat-square&label=installs)
![MIT License](https://img.shields.io/packagist/l/mobiledetect/mobiledetectlib?style=flat-square)
Mobile Detect is a lightweight PHP class for detecting mobile devices (including tablets).
It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.
- MobileDetect class is a
[server-side detection](http://www.w3.org/TR/mwabp/#bp-devcap-detection) PHP class that can help you with your RWD strategy,
it is not a replacement for CSS3 media queries or other forms of client-side feature detection.
- Can detect the difference between a mobile phone and a table by using regexes.
- The accuracy and relevance of the detection is kept by running [tests](./tests) to check for detection conflicts.
```php
$detect = new \Detection\MobileDetect;
$deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer');
```
## Installing
### Download and include manually
You need PHP >= `7.4`.
- [Download the latest release](../../tags)
- Get the PHP class [MobileDetect.php](src/MobileDetect.php) and include it in your PHP source.
### Composer
- Install via [composer](https://packagist.org/packages/mobiledetect/mobiledetectlib).
`composer require mobiledetect/mobiledetectlib:3.74`
- Include the dependency in the `composer.json` file:
```json
{
"require": {
"mobiledetect/mobiledetectlib": "^3.74"
}
}
```
## Contribute
- Go to * [:iphone: Live demo](https://demo.mobiledetect.net)
- Read [how to contribute](docs/CONTRIBUTING.md) guide.
- Submit a pull request.
*Donate*
|Paypal|
|------|
|[Donate :+1:](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=mobiledetectlib%40gmail%2ecom&lc=US&item_name=Mobile%20Detect&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donate_SM%2egif%3aNonHosted)|
I'm currently paying for hosting and spend a lot of my family time to maintain the project and planning the future releases.
I would highly appreciate any money donations that will keep the research going.
Special thanks to the community :+1: for donations, JetBrains team for the continuous support and [Dragos Gavrila](https://twitter.com/grafician) who contributed with the logo.
## Modules, plugins, ports
[Submit new module, plugin, port](../../issues/new?title=New%203rd%20party%20module&body=Name,%20Link%20and%20Description%20of%20the%20module.)
**JavaScript**
* mobile-detect.js - A [JavaScript port](https://github.com/hgoebl/mobile-detect.js) of Mobile-Detect class. Made by [Heinrich Goebl](https://github.com/hgoebl).
**Varnish Cache**
* [Varnish Mobile Detect](https://github.com/willemk/varnish-mobiletranslate) - Drop-in varnish solution to mobile user
detection based on the Mobile-Detect library. Made by [willemk](https://github.com/willemk).
* [mobiledetect2vcl](https://github.com/carlosabalde/mobiledetect2vcl) - Python script to transform the Mobile
Detect JSON database into an UA-based mobile detection VCL subroutine easily integrable in any Varnish Cache
configuration. Made by [Carlos Abalde](https://github.com/carlosabalde).
**LUA**
* [mobile-detect.lua](https://github.com/yourpalmark/mobile-detect.lua) is a port of Mobile-Detect to Lua for
NGINX HTTP servers. Follows closely to mobile-detect.js. Supports all methods that server-side
mobile-detect.js supports. Fully unit-tested and synced with Travis CI (Build Passing badge included).
Made by [Mark Walters](https://github.com/yourpalmark).
**PHP**
**WordPress**
* [Mobile Detect for WordPress](https://wordpress.org/plugins/tinywp-mobile-detect/) - WordPress has a built-in function
(`wp_is_mobile()`) to detect mobile devices. There is (at least) one catch, though. It considers iPad (iPad pro,
and any tablet) as a mobile. So, this 3rd party module changes the way `wp_is_mobile()` works with the help of
Mobile Detect PHP library!
Made by [Pothi Kalimuthu](https://github.com/pothi)
* [WordPress Mobile Detect](https://wordpress.org/plugins/wp-mobile-detect/) - Gives you the ability to wrap that
infographic in a `[notdevice][/notdevice]` shortcode so at the server level WordPress will
decide to show that content only if the user is NOT on a phone or tablet.
Made by [Jesse Friedman](https://profiles.wordpress.org/professor44/).
* [mobble](https://wordpress.org/plugins/mobble/) - provides mobile related conditional functions for your site.
e.g. `is_iphone()`, `is_mobile()` and `is_tablet()`. Made by Scott Evans.
* [WordPress Responsage](https://github.com/iamspacehead/responsage) - A small WordPress theme plugin that allows
you to make your images responsive. Made by [Adrian Ciaschetti](https://github.com/iamspacehead).
* [WP247 Body Classes](https://wordpress.org/plugins/wp247-body-classes/) - Add unique classes to the `body` tag for
easy styling based on various attributes (archive, user, post, mobile) and various WordPress "is" functions.
Mobile attributes include type of device, Operating System, Browser, etc. Examples: .is-mobile, .is-not-mobile,
.is-tablet, .is-ios, .is-not-ios, .is-androidos, .is-chromebrowser.
Made by [wescleveland56](https://github.com/wescleveland56).
* [Adaptive Content](https://wordpress.org/plugins/addfunc-adaptive-content/) for WordPress provides the most
intuitive set of shortcodes for including/excluding content on mobile devices, tablets desktops and other
more specific device parameters. This lightweight plugin lets content writers and theme authors choose when
WordPress should or shouldnt show any give content item using shortcodes and quicktags or theme elements using functions.
Made by [AddFunc](https://profiles.wordpress.org/addfunc).
* [AddFunc Mobile Detect](https://wordpress.org/plugins/addfunc-mobile-detect/) for WordPress redirects
mobile traffic to your mobile website and, basically, gives you loads of control over your mobile redirects.
Made by [AddFunc](https://profiles.wordpress.org/addfunc).
**Drupal**
* [Drupal Mobile Switch](https://www.drupal.org/project/mobile_switch) - The Mobile Switch Drupal module provides a
automatic theme switch functionality for mobile devices, detected by Browscap or Mobile Detect.
Made by [Siegfried Neumann](https://www.drupal.org/user/45267).
* [Drupal Context Mobile Detect](https://www.drupal.org/project/context_mobile_detect) - This is a Drupal context module
which integrates Context and PHP Mobile Detect library.
Created by [Artem Shymko](https://www.drupal.org/user/432492).
* [Drupal Mobile Detect](https://www.drupal.org/project/mobile_detect) - Lightweight mobile detect module for Drupal
created by [Matthew Donadio](https://www.drupal.org/user/325244).
**Joomla**
* [yagendoo Joomla! Mobile Detection Plugin](http://www.yagendoo.com/en/blog/free-mobile-detection-plugin-for-joomla.html) - Lightweight PHP plugin for Joomla!
that detects a mobile browser using the Mobile Detect class.
Made by yagendoo media.
* [User Agent Detector plugin](https://github.com/renekreijveld/UserAgentDetector) - This system plugin detects the user
agent of your website visitor and sets a session variable accordingly. Based on the user agent, the plugin detects if the
site is running on a desktop pc, tablet or smartphone. It can also detect if the visitor is a spider bot (search engine).
Session variable that is set: `ualayout`. Possible values: desktop, tablet, mobile, bot.
Made by @ReneKreijveld.
**Magento**
* [Magento helper](http://www.magentocommerce.com/magento-connect/catalog/product/view/id/16835/) from Optimise Web enables
the use of all functions provided by Mobile Detect. Made by [Kathir Vel](http://www.kathirvel.com).
* [Magento 2 Mobile Detect Theme Change](https://github.com/EaDesgin/magento2-mobiledetect) is an extension for Magento 2
that will change the theme or redirect to a different URL. Also containing a helper to check for the device type.
**PrestaShop**
* [PrestaShop](https://www.prestashop.com) is a free, secure and open source shopping cart platform. Mobile_Detect
is included in the default package since 1.5.x.
**Laravel**
* [Agent](https://github.com/jenssegers/agent) is a user agent class for Laravel based on Mobile Detect with some
additional functionality.
Made by [Jens Segers](https://github.com/jenssegers).
* [Laravel Mobile Detect](https://github.com/riverskies/laravel-mobile-detect) is a package that enables you to use device detection right in your Laravel Blade templates. (Utilises the well-known, constantly updated PHP mobile detection library.)
Made by [Barnabas Kecskes](https://github.com/barnabaskecskes).
* [BrowserDetect](https://github.com/hisorange/browser-detect) is a browser and mobile detection package, collects
and wrap together the best user-agent identifiers for Laravel.
Created by [Varga Zsolt](https://github.com/hisorange).
**Zend Framework**
* [ZF2 Mobile-Detect](https://github.com/neilime/zf2-mobile-detect.git) is a Zend Framework 2 module that provides
Mobile-Detect features (Mobile_Detect class as a service, helper for views and plugin controllers).
Made by [neilime](https://github.com/neilime).
* [ZF2 MobileDetectModule](https://github.com/nikolaposa/MobileDetectModule) facilitates integration of a PHP MobileDetect
class with some ZF2-based application. Has similar idea like the existing ZF2 Mobile-Detect module,
but differs in initialization and provision routine of the actual Mobile_Detect class.
Appropriate view helper and controller plugin also have different conceptions.
Made by [Nikola Posa](https://github.com/nikolaposa).
**Symfony**
* [Symfony3 MobileDetectBundle](https://github.com/tattali/MobileDetectBundle) is a Symfony bundle to detect mobile devices, manage mobile view and redirect to the mobile and tablet version. Made by [Théo Attali](https://github.com/tattali).
* [Symfony2 Mobile Detect Bundle](https://github.com/suncat2000/MobileDetectBundle) is a bundle for detecting mobile devices,
manage mobile view and redirect to the mobile and tablet version.
Made by [Nikolay Ivlev](https://github.com/suncat2000).
* [Silex Mobile Detect Service Provider](https://github.com/jbinfo/MobileDetectServiceProvider) is a service provider to
interact with Mobile detect class methods.
Made by [Lhassan Baazzi](https://github.com/jbinfo).
**Slim Framework**
* [Slim_Mobile_Detect](https://github.com/zguillez/slim_mobile_detect) implements Mobile_Detect lib for different
responses write on Slim Framework App.
**ExpressionEngine**
* [EE2 Detect Mobile](https://github.com/garethtdavies/detect-mobile) is a lightweight PHP plugin for EE2 that detects
a mobile browser using the Mobile Detect class. Made by [Gareth Davies](https://github.com/garethtdavies).
**Yii Framework**
* [Yii Extension](https://github.com/iamsalnikov/MobileDetect) - Mobile detect plugin for Yii framework.
Made by [Alexey Salnikov](https://github.com/iamsalnikov).
* [Yii Extension](https://github.com/candasm/yii1-mobile-detect-component) - Mobile detect component for Yii framework
1.x version which supports composer package manager. Made by [Candas Minareci](https://github.com/candasm).
* [Yii2 Device Detect](https://github.com/alexandernst/yii2-device-detect/) - Yii2 extension for Mobile-Detect library.
Made by [Alexander Nestorov](https://github.com/alexandernst).
**CakePHP**
* [CakePHP MobileDetect](https://github.com/chronon/CakePHP-MobileDetectComponent-Plugin) is a plugin component for
CakePHP 2.x. Made by [Gregory Gaskill](https://github.com/chronon).
**FuelPHP**
* [Special Agent](https://github.com/rob-bar/special_agent) is a FuelPHP package which uses php-mobile-detect to
determine whether a device is mobile or not. It overrides the Fuelphp Agent class its methods.
Made by [Robbie Bardjin](https://github.com/rob-bar).
**TYPO3**
* [px_mobiledetect](https://typo3.org/extensions/repository/view/px_mobiledetect) is an extension that helps to detect
visitor's mobile device class (if thats tablet or mobile device like smartphone). Made by Alexander Tretyak.
**Other**
* [PageCache](https://github.com/mmamedov/page-cache) is a lightweight PHP library for full page cache,
with built-in Mobile-Detect support. Made by [Muhammed Mamedov](https://github.com/mmamedov).
* [Statamic CMS Mobile Detect](https://github.com/haikulab/statamic-mobile-detect) is a plugin.
Made by [Sergei Filippov](https://github.com/haikulab/statamic-mobile-detect) of Haiku Lab.
* [Kohana Mobile Detect](https://github.com/madeinnordeste/kohana-mobile-detect) is an example of implementation of
Mobile_Detect class with Kohana framework.
Written by [Luiz Alberto S. Ribeiro](https://github.com/madeinnordeste).
* [MemHT](https://www.memht.com) is a Free PHP CMS and Blog that permit the creation and the management online
of websites with few and easy steps. Has the class included in the core.
* [concrete5](https://www.concrete5.org) is a CMS that is free and open source. The library is included in the core.
* [engine7](https://github.com/QOXCorp/exengine) is PHP Open Source Framework. The Mobile_Detect class is included in
the engine.
* [Zikula](http://zikula.org) is a free and open-source Content Management Framework, which allows you to run
impressive websites and build powerful online applications. The core uses Mobile-Detect to switch to a special
Mobile theme, using jQueryMobile.
* [UserAgentInfo](https://github.com/quentin389/UserAgentInfo) is a PHP class for parsing user agent strings
(HTTP_USER_AGENT). Includes mobile checks, bot checks, browser types/versions and more.
Based on browscap, Mobile_Detect and ua-parser. Created for high traffic websites and fast batch processing.
Made by [quentin389](https://github.com/quentin389).
* [LJ Mobile Detect](https://github.com/lewisjenkins/craft-lj-mobiledetect) is a simple implementation of Mobile Detect
for Craft CMS. Made by [Lewis Jenkins](https://github.com/lewisjenkins).
* [Detect Craft](https://github.com/mmikkel/Detect-Craft) is a Craft CMS wrapper for the Mobile_Detect library. Made by [Mikkel Rummelhoff](https://github.com/mmikkel).
* [Grav Plugin Mobile Detect](https://github.com/dimitrilongo/grav-plugin-mobile-detect/) is a simple implementation
of Mobile Detect for Grav CMS. Made by [Dimitri Longo](https://github.com/dimitrilongo).
* [Mobile_Detect module for UliCMS](https://github.com/derUli/ulicms-Mobile_Detect).
Made by [derUli](https://github.com/derUli).
**Perl**
* [MobileDetect.pm](https://www.buzzerstar.com/development/) is a Perl module for Mobile Detect.
Made by [Sebastian Enger](https://devop.tools/).
**Python**
* [pymobiledetect](https://pypi.python.org/pypi/pymobiledetect) - Mobile detect python package.
Made by Bas van Oostveen.
**Ruby**
* [mobile_detect.rb](https://github.com/ktaragorn/mobile_detect) is a Ruby gem using the JSON data exposed by the
php project and implementing a basic subset of the API (as much as can be done by the exposed data).
Made by [Karthik T](https://github.com/ktaragorn).
**Go**
* [GoMobileDetect](https://github.com/Shaked/gomobiledetect) is a Go port of Mobile Detect class.
Made by [https://github.com/Shaked](Shaked).
**LUA**
* [ua-lua](https://github.com/robinef/ua-lua) is a small lib written in LUA providing device type detection.
ua-lua is detecting mobile or tablet devices based on user-agent inside nginx daemon.
Made by [Frédéric Robinet](https://github.com/robinef).
**.Net**
* [mobile-detect](https://github.com/validide/mobile-detect) is a .Net partial port written in C#.
Made by [Valentin Dide](https://github.com/validide).
**ColdFusion**
* [MobileDetect](https://github.com/GiancarloGomez/ColdFusion-MobileDetect) is a CFC port of the
Mobile_Detect PHP Library. Made by [Giancarlo Gomez](https://github.com/GiancarloGomez).
**Experiments** :bulb:
* [Mobile Detect Fast](https://bitbucket.org/lanaguani/mobile-detect-fast/) (See: [#474](https://github.com/serbanghita/Mobile-Detect/issues/474)) is a class to increase the performance of Mobile Detect lib. Made by [LanaGuani](https://github.com/lanaguanifw).
## About
See [the history](./docs/HISTORY.md) of the project.
+40
View File
@@ -0,0 +1,40 @@
{
"name": "mobiledetect/mobiledetectlib",
"type": "library",
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
"keywords": ["mobile", "mobile detect", "mobile detector", "php mobile detect", "detect mobile devices"],
"homepage": "https://github.com/serbanghita/Mobile-Detect",
"license": "MIT",
"authors": [
{
"name": "Serban Ghita",
"email": "serbanghita@gmail.com",
"homepage": "https://mobiledetect.net",
"role": "Developer"
}
],
"require": {
"php": ">=7.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.14",
"phpunit/phpunit": "^9.6",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"classmap": [
"src/MobileDetect.php"
],
"psr-4": {
"Detection\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"DetectionTests\\": "tests/"
}
},
"archive": {
"exclude": ["docs", "scripts"]
}
}
+16
View File
@@ -0,0 +1,16 @@
app:
restart: 'on-failure'
image: php:7
working_dir: /app
command: vendor/bin/phpunit -v -c tests/phpunit.xml --coverage-text --strict-coverage --stop-on-risky
ports:
- "8000:8000"
volumes:
- .:/app
composer:
restart: 'no'
image: composer/composer:php7
command: install
volumes:
- .:/app
+1419
View File
File diff suppressed because one or more lines are too long
+101
View File
@@ -0,0 +1,101 @@
<?php
/**
* 生成 check_config/*.phpdefine 拼接,输出格式与历史 dashboard 一致)
*/
class ConfigWriter
{
/**
* @param string $checkName 配置名,如 index.php
* @param array $data 表单字段
* @return string PHP 配置源码
*/
public static function buildDefineSource($checkName, array $data)
{
$methods = $data['methods'];
$blacklist_group_id = (int) $data['blacklist_group_id'];
$whitelist_group_id = (int) $data['whitelist_group_id'];
$country = $data['country'];
$show_content = $data['show_content'];
$DB_fp = $data['DB_fp'];
$costm_ip_score = $data['costm_ip_score'];
$auto_black = $data['auto_black'];
$is_virtual = $data['is_virtual'];
$auto_black_times = $data['auto_black_times'];
$white_params = $data['white_params'];
$iphone_model = $data['iphone_model'];
$write_log = $data['write_log'];
$zh_on = $data['zh_on'];
$os_on = $data['os_on'];
$keep_params = $data['keep_params'];
$redirect_method = $data['redirect_method'];
$DB_zp = $data['DB_zp'];
$mobile_on = $data['mobile_on'];
$v_referer = $data['v_referer'];
$risk_number = $data['risk_number'];
$risk_enhanced = $data['risk_enhanced'];
$url_args_timeout = $data['url_args_timeout'];
$debug_mode = $data['debug_mode'];
$ad_fb = $data['ad_fb'];
$ad_google = $data['ad_google'];
$ad_tiktok = $data['ad_tiktok'];
$ad_strict = $data['ad_strict'];
$ad_spm_id = $data['ad_spm_id'];
// deprecated:业务代码未读取,保留 define 以兼容旧工具链
$mobile = 0;
$output = 1;
return "<?php
define('SHOW_SITE_MODE_SWITCH', '" . $methods . "' );
define('SHOW_SITE_IP', '' );
define('BLACKLIST_GROUP_ID', " . $blacklist_group_id . " );
define('WHITELIST_GROUP_ID', " . $whitelist_group_id . " );
define('SHOW_SITE_COUNTRY', '" . $country . "' );
define('CLOAK_SHOW_CONTENT', '" . $show_content . "');
define('DB_FP', " . var_export($DB_fp, true) . " );
define('COSTM_IP_SCORE', '" . $costm_ip_score . "' );
define('BLACK_LIST', '0' );
define('AUTO_BLACK', '" . $auto_black . "' );
define('IS_VIRTUAL', '" . $is_virtual . "' );
define('AUTO_BLACK_TIMES', '" . $auto_black_times . "' );
define('WHITE_PARAMS', '" . $white_params . "' );
define('IPHONE_MODEL', " . $iphone_model . " );
define('SHOW_SITE_MOBILE', " . $mobile . " );
define('SHOW_SITE_OUTPUT', " . $output . " );
define('WRITE_LOG', " . $write_log . " );
define('CLOAK_ZH_ON', '" . $zh_on . "');
define('CLOAK_OS_ON', '" . $os_on . "');
define('KEEP_PARAMS', '" . $keep_params . "');
define('CLOAK_REDIRECT_METHOD', '" . $redirect_method . "');
define('DB_ZP', '" . $DB_zp . "');
define('CLOAK_MOBILE_ON', '" . $mobile_on . "');
define('CLOAK_V_REFERER', '" . $v_referer . "');
define('CLOAK_RISK_NUMBER', '" . $risk_number . "');
define('CLOAK_RISK_ENHANCED', '" . $risk_enhanced . "');
define('CLOAK_URL_ARGS_TIMEOUT', '" . $url_args_timeout . "');
define('CLOAK_AD_FB', '" . $ad_fb . "');
define('CLOAK_AD_GOOGLE', '" . $ad_google . "');
define('CLOAK_AD_TIKTOK', '" . $ad_tiktok . "');
define('CLOAK_AD_STRICT', '" . $ad_strict . "');
define('CLOAK_AD_SPM_ID', '" . $ad_spm_id . "');
define('CLOAK_DEBUG_MODE', '" . $debug_mode . "');
?" . ">";
}
public static function save($checkName, array $data)
{
$str = self::buildDefineSource($checkName, $data);
$config_name = str_replace('.php', '', $checkName);
$config_file = 'check_config/' . $config_name . '_config.php';
if (!file_exists($config_file)) {
copy('check_config/index_config.php', $config_file);
}
$f = fopen($config_file, 'w');
rewind($f);
fwrite($f, $str);
fclose($f);
}
}
+11
View File
@@ -0,0 +1,11 @@
<?php
/**
* 后台公共引导:协议检测等
*/
if ((isset($_SERVER['HTTPS']) && (($_SERVER['HTTPS'] == 'on') || ($_SERVER['HTTPS'] == '1'))) || $_SERVER['SERVER_PORT'] == 443) {
$protocol = 'https://';
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') {
$protocol = 'https://';
} else {
$protocol = 'http://';
}
+538
View File
@@ -0,0 +1,538 @@
<?php
/**
* IP 黑白名单管理(单文件)
* 依赖 cong.php 中的数据库常量与 LOGIN_PASSWORD
*
* INSERT IGNORE 去重依赖唯一约束,若尚未建立可执行:
* ALTER TABLE ip_list ADD UNIQUE KEY uk_group_ip (group_id, ip_address);
*/
declare(strict_types=1);
require_once __DIR__ . '/cong.php';
session_start();
header('Content-Type: text/html; charset=utf-8');
if (empty($_SESSION['password']) || $_SESSION['password'] !== LOGIN_PASSWORD) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['admin_password'])) {
if (trim((string) $_POST['admin_password']) === LOGIN_PASSWORD) {
$_SESSION['password'] = LOGIN_PASSWORD;
header('Location: ' . strtok($_SERVER['REQUEST_URI'] ?? 'admin_ip_manager.php', '?'));
exit;
}
$loginError = '密码错误';
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登录 — IP 名单管理</title>
<style>
body { font-family: system-ui, sans-serif; max-width: 360px; margin: 4rem auto; padding: 0 1rem; }
input[type=password] { width: 100%; padding: 0.5rem; box-sizing: border-box; margin: 0.5rem 0; }
button { padding: 0.5rem 1rem; cursor: pointer; }
.err { color: #c00; margin-bottom: 1rem; }
</style>
</head>
<body>
<h1>IP 名单管理</h1>
<?php if (!empty($loginError)) { ?><p class="err"><?php echo htmlspecialchars($loginError, ENT_QUOTES, 'UTF-8'); ?></p><?php } ?>
<form method="post">
<label>后台密码</label>
<input type="password" name="admin_password" required autocomplete="current-password">
<button type="submit">进入</button>
</form>
<p><small>与 dashboard 使用相同登录密码;若已在后台登录可刷新本页。</small></p>
</body>
</html>
<?php
exit;
}
$type = (isset($_GET['type']) && $_GET['type'] === 'white') ? 'white' : 'black';
$self = htmlspecialchars($_SERVER['PHP_SELF'] ?? 'admin_ip_manager.php', ENT_QUOTES, 'UTF-8');
$flash = '';
$flashOk = true;
try {
$dsn = 'mysql:host=127.0.0.1;dbname=' . DB_NAME . ';charset=utf8mb4';
$pdo = new PDO($dsn, DB_USERNAME, DB_PASSWORD, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]);
} catch (PDOException $e) {
http_response_code(500);
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>错误</title></head><body><p>数据库连接失败。</p></body></html>';
exit;
}
/** 校验分组属于当前 type,返回 true 或 false */
function groupBelongsToType(PDO $pdo, int $groupId, string $type): bool
{
$st = $pdo->prepare('SELECT 1 FROM ip_groups WHERE id = ? AND type = ? LIMIT 1');
$st->execute([$groupId, $type]);
return (bool) $st->fetchColumn();
}
// —— POST:新建分组 ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'create_group') {
$name = trim((string) ($_POST['group_name'] ?? ''));
$postType = ($_POST['list_type'] ?? '') === 'white' ? 'white' : 'black';
if ($name === '') {
$flash = '分组名称不能为空';
$flashOk = false;
} else {
$st = $pdo->prepare('INSERT INTO ip_groups (name, type) VALUES (?, ?)');
$st->execute([$name, $postType]);
$flash = '分组已创建';
header('Location: ' . $self . '?type=' . urlencode($postType) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
// —— POST:批量导入 IP ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'bulk_import') {
$groupId = (int) ($_POST['import_group_id'] ?? 0);
$raw = (string) ($_POST['ip_lines'] ?? '');
if ($groupId <= 0 || !groupBelongsToType($pdo, $groupId, $type)) {
$flash = '请选择有效的分组';
$flashOk = false;
} else {
$lines = preg_split('/\R/u', $raw) ?: [];
$uniqueFromText = [];
foreach ($lines as $line) {
$ip = trim($line);
if ($ip === '') {
continue;
}
$uniqueFromText[$ip] = true;
}
$candidateIps = array_keys($uniqueFromText);
$exSt = $pdo->prepare('SELECT ip_address FROM ip_list WHERE group_id = ?');
$exSt->execute([$groupId]);
$already = [];
while ($row = $exSt->fetch(PDO::FETCH_ASSOC)) {
$already[$row['ip_address']] = true;
}
$toInsert = [];
foreach ($candidateIps as $ip) {
if (!isset($already[$ip])) {
$toInsert[] = $ip;
}
}
$inserted = 0;
$stmt = $pdo->prepare('INSERT IGNORE INTO ip_list (group_id, ip_address) VALUES (?, ?)');
$pdo->beginTransaction();
try {
foreach ($toInsert as $ip) {
$stmt->execute([$groupId, $ip]);
$inserted += $stmt->rowCount();
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '导入失败:' . $e->getMessage();
$flashOk = false;
}
if ($flashOk) {
$skippedDupText = count($candidateIps) - count($toInsert);
$flash = '批量导入完成:新插入 ' . $inserted . ' 条;文本去重后 ' . count($candidateIps) . ' 条;本组已存在跳过 ' . $skippedDupText . ' 条';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
}
// —— POST:按行批量删除 IP(当前 type 下;可选限定分组)——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'bulk_delete_ips') {
$scopeGroup = (int) ($_POST['bulk_delete_group_id'] ?? 0);
$raw = (string) ($_POST['bulk_delete_lines'] ?? '');
if ($scopeGroup > 0 && !groupBelongsToType($pdo, $scopeGroup, $type)) {
$flash = '批量删除:分组无效';
$flashOk = false;
} else {
$lines = preg_split('/\R/u', $raw) ?: [];
$ips = [];
foreach ($lines as $line) {
$ip = trim($line);
if ($ip !== '') {
$ips[$ip] = true;
}
}
$ips = array_keys($ips);
if ($ips === []) {
$flash = '批量删除:未填写任何 IP';
$flashOk = false;
} else {
$sql = 'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ? AND il.ip_address = ?';
if ($scopeGroup > 0) {
$sql .= ' AND il.group_id = ?';
}
$stmt = $pdo->prepare($sql);
$deleted = 0;
$pdo->beginTransaction();
try {
foreach ($ips as $ip) {
if ($scopeGroup > 0) {
$stmt->execute([$type, $ip, $scopeGroup]);
} else {
$stmt->execute([$type, $ip]);
}
$deleted += $stmt->rowCount();
}
$pdo->commit();
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '批量删除失败:' . $e->getMessage();
$flashOk = false;
}
if ($flashOk) {
$flash = '批量删除完成,共删除 ' . $deleted . ' 条记录(不存在的 IP 不计入)';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
}
}
// —— POST:清空当前类型下所有 IP(保留分组)——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clear_ips_only') {
$phrase = trim((string) ($_POST['clear_confirm_phrase'] ?? ''));
if ($phrase !== '清空IP') {
$flash = '清空失败:请在确认框中完整输入「清空IP」';
$flashOk = false;
} else {
$del = $pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?'
);
$del->execute([$type]);
$flash = '已清空当前类型下所有 IP(分组已保留),删除 ' . $del->rowCount() . ' 条';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
}
}
// —— POST:清空当前类型下全部分组及 IP ——
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clear_type_all') {
$phrase = trim((string) ($_POST['clear_confirm_phrase_all'] ?? ''));
if ($phrase !== '清空全部') {
$flash = '清空失败:请在确认框中完整输入「清空全部」';
$flashOk = false;
} else {
$pdo->beginTransaction();
try {
$pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?'
)->execute([$type]);
$pdo->prepare('DELETE FROM ip_groups WHERE type = ?')->execute([$type]);
$pdo->commit();
$flash = '已清空当前类型下的所有分组及 IP';
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . '&ok=1');
exit;
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '清空失败:' . $e->getMessage();
$flashOk = false;
}
}
}
// —— GET:删除分组 ——
if (isset($_GET['delete_group'])) {
$gid = (int) $_GET['delete_group'];
if ($gid > 0 && groupBelongsToType($pdo, $gid, $type)) {
$pdo->beginTransaction();
try {
$pdo->prepare('DELETE FROM ip_list WHERE group_id = ?')->execute([$gid]);
$pdo->prepare('DELETE FROM ip_groups WHERE id = ? AND type = ?')->execute([$gid, $type]);
$pdo->commit();
$flash = '分组及其 IP 已删除';
} catch (Throwable $e) {
$pdo->rollBack();
$flash = '删除分组失败';
$flashOk = false;
}
} else {
$flash = '无效的分组';
$flashOk = false;
}
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . ($flashOk ? '&ok=1' : ''));
exit;
}
// —— GET:删除单条 IP ——
if (isset($_GET['delete_ip'])) {
$ipId = (int) $_GET['delete_ip'];
if ($ipId > 0) {
$del = $pdo->prepare(
'DELETE il FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE il.id = ? AND ig.type = ?'
);
$del->execute([$ipId, $type]);
$flash = $del->rowCount() > 0 ? '已删除该 IP 记录' : '未找到记录或无权删除';
$flashOk = $del->rowCount() > 0;
} else {
$flash = '无效的 IP 记录';
$flashOk = false;
}
header('Location: ' . $self . '?type=' . urlencode($type) . '&msg=' . urlencode($flash) . ($flashOk ? '&ok=1' : ''));
exit;
}
if (isset($_GET['msg'])) {
$flash = (string) $_GET['msg'];
$flashOk = isset($_GET['ok']) && $_GET['ok'] === '1';
}
// 当前 type 下的分组
$groupsStmt = $pdo->prepare('SELECT id, name FROM ip_groups WHERE type = ? ORDER BY id ASC');
$groupsStmt->execute([$type]);
$groups = $groupsStmt->fetchAll();
$ipSearch = isset($_GET['ip_search']) ? trim((string) $_GET['ip_search']) : '';
$ipSearchSql = '';
$ipListBind = [$type];
if ($ipSearch !== '') {
$ipSearchSql = ' AND il.ip_address LIKE ?';
$ipListBind[] = '%' . addcslashes($ipSearch, '\\%_') . '%';
}
$searchQuerySuffix = $ipSearch !== '' ? '&ip_search=' . rawurlencode($ipSearch) : '';
// IP 列表分页
$perPage = 50;
$page = max(1, (int) ($_GET['page'] ?? 1));
$offset = ($page - 1) * $perPage;
$countSql = 'SELECT COUNT(*) FROM ip_list il INNER JOIN ip_groups ig ON ig.id = il.group_id WHERE ig.type = ?' . $ipSearchSql;
$totalStmt = $pdo->prepare($countSql);
$totalStmt->execute($ipListBind);
$totalIps = (int) $totalStmt->fetchColumn();
$totalPages = max(1, (int) ceil($totalIps / $perPage));
if ($page > $totalPages) {
$page = $totalPages;
$offset = ($page - 1) * $perPage;
}
$listSql = 'SELECT il.id, il.ip_address, il.group_id, ig.name AS group_name
FROM ip_list il
INNER JOIN ip_groups ig ON ig.id = il.group_id
WHERE ig.type = ?' . $ipSearchSql . '
ORDER BY il.id DESC
LIMIT ' . (int) $perPage . ' OFFSET ' . (int) $offset;
$listStmt = $pdo->prepare($listSql);
$listStmt->execute($ipListBind);
$ipRows = $listStmt->fetchAll();
function h(string $s): string
{
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
$typeParam = 'type=' . urlencode($type);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IP 名单管理 — <?php echo $type === 'white' ? '白名单' : '黑名单'; ?></title>
<style>
:root { --bd: #ccc; --bg: #f6f7f9; --ok: #0a0; --err: #c00; }
body { font-family: system-ui, -apple-system, sans-serif; margin: 0; padding: 1rem 1.25rem 2rem; background: var(--bg); color: #222; }
h1 { font-size: 1.35rem; margin-top: 0; }
h2 { font-size: 1.1rem; margin: 1.5rem 0 0.75rem; border-bottom: 1px solid var(--bd); padding-bottom: 0.35rem; }
.tabs { display: flex; gap: 0.5rem; margin-bottom: 1.25rem; flex-wrap: wrap; }
.tabs a {
display: inline-block; padding: 0.45rem 1rem; text-decoration: none; border-radius: 6px;
border: 1px solid var(--bd); background: #fff; color: #111;
}
.tabs a.active { background: #111; color: #fff; border-color: #111; }
.flash { padding: 0.65rem 1rem; border-radius: 6px; margin-bottom: 1rem; }
.flash.ok { background: #e8f5e9; color: #1b5e20; }
.flash.bad { background: #ffebee; color: #b71c1c; }
section.card { background: #fff; border: 1px solid var(--bd); border-radius: 8px; padding: 1rem 1.1rem; margin-bottom: 1rem; max-width: 960px; }
label { display: block; font-weight: 600; margin: 0.5rem 0 0.25rem; font-size: 0.9rem; }
input[type=text], select, textarea { width: 100%; max-width: 100%; box-sizing: border-box; padding: 0.45rem 0.5rem; border: 1px solid var(--bd); border-radius: 4px; font-size: 0.95rem; }
textarea { min-height: 160px; font-family: ui-monospace, monospace; }
button, .btn-link {
display: inline-block; margin-top: 0.5rem; padding: 0.45rem 0.9rem; cursor: pointer; border-radius: 4px;
border: 1px solid var(--bd); background: #fff; font-size: 0.9rem; text-decoration: none; color: #111;
}
button.primary { background: #1976d2; color: #fff; border-color: #1565c0; }
button.danger, a.danger { color: var(--err); border-color: #e57373; }
table { width: 100%; border-collapse: collapse; font-size: 0.9rem; }
th, td { border: 1px solid var(--bd); padding: 0.45rem 0.55rem; text-align: left; }
th { background: #eee; }
tr:nth-child(even) { background: #fafafa; }
.pager { margin-top: 1rem; display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; }
.muted { color: #666; font-size: 0.85rem; }
.row-inline { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: flex-end; }
.row-inline > div { flex: 1; min-width: 200px; }
section.card.danger-zone { border-color: #e57373; background: #fff8f8; }
section.card.danger-zone h2 { color: #b71c1c; }
</style>
</head>
<body>
<h1>IP 黑白名单管理</h1>
<nav class="tabs">
<a href="<?php echo $self; ?>?type=black<?php echo $searchQuerySuffix; ?>" class="<?php echo $type === 'black' ? 'active' : ''; ?>">黑名单管理</a>
<a href="<?php echo $self; ?>?type=white<?php echo $searchQuerySuffix; ?>" class="<?php echo $type === 'white' ? 'active' : ''; ?>">白名单管理</a>
</nav>
<?php if ($flash !== '') { ?>
<div class="flash <?php echo $flashOk ? 'ok' : 'bad'; ?>"><?php echo h($flash); ?></div>
<?php } ?>
<section class="card">
<h2>新建分组</h2>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>">
<input type="hidden" name="action" value="create_group">
<input type="hidden" name="list_type" value="<?php echo h($type); ?>">
<label for="group_name">组名</label>
<input type="text" id="group_name" name="group_name" required maxlength="191" placeholder="例如:广告渠道 A">
<button type="submit" class="primary">创建分组(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?></button>
</form>
</section>
<section class="card">
<h2>当前视图下的分组</h2>
<?php if ($groups === []) { ?>
<p class="muted">暂无分组,请先创建。</p>
<?php } else { ?>
<table>
<thead>
<tr><th>ID</th><th>组名</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($groups as $g) { ?>
<tr>
<td><?php echo (int) $g['id']; ?></td>
<td><?php echo h((string) $g['name']); ?></td>
<td>
<a class="btn-link danger" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;delete_group=<?php echo (int) $g['id']; ?>"
onclick="return confirm(<?php echo json_encode('确定删除分组「' . (string) $g['name'] . '」及其下所有 IP', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>);">删除组</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php } ?>
</section>
<section class="card">
<h2>批量导入 IP</h2>
<p class="muted">每行一个 IP;首尾空格与空行忽略;同一文本内重复只计一次;本组数据库中已存在的 IP 不会重复插入。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>">
<input type="hidden" name="action" value="bulk_import">
<label for="import_group_id">目标分组</label>
<select id="import_group_id" name="import_group_id" required>
<option value="">— 请选择 —</option>
<?php foreach ($groups as $g) { ?>
<option value="<?php echo (int) $g['id']; ?>"><?php echo h((string) $g['name']); ?> (ID <?php echo (int) $g['id']; ?>)</option>
<?php } ?>
</select>
<label for="ip_lines">IP 列表</label>
<textarea id="ip_lines" name="ip_lines" placeholder="1.2.3.4&#10;2001:db8::1"></textarea>
<button type="submit" class="primary">导入到所选分组</button>
</form>
</section>
<section class="card">
<h2>批量删除 IP</h2>
<p class="muted">每行一个 IP;同一 IP 在文本中多次出现会去重后删除。选择「不限定分组」时,会删除当前<?php echo $type === 'white' ? '白' : '黑'; ?>名单类型下所有分组中匹配的 IP。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" onsubmit="return confirm('确定按列表批量删除?');">
<input type="hidden" name="action" value="bulk_delete_ips">
<label for="bulk_delete_group_id">作用范围</label>
<select id="bulk_delete_group_id" name="bulk_delete_group_id">
<option value="0">不限定分组(当前类型下全部匹配)</option>
<?php foreach ($groups as $g) { ?>
<option value="<?php echo (int) $g['id']; ?>">仅分组:<?php echo h((string) $g['name']); ?> (ID <?php echo (int) $g['id']; ?>)</option>
<?php } ?>
</select>
<label for="bulk_delete_lines">要删除的 IP 列表</label>
<textarea id="bulk_delete_lines" name="bulk_delete_lines" placeholder="1.2.3.4&#10;5.6.7.8"></textarea>
<button type="submit" class="danger">执行批量删除</button>
</form>
</section>
<section class="card danger-zone">
<h2>清空数据(危险操作)</h2>
<p class="muted">以下操作仅影响当前视图类型(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?>),不影响另一类型。</p>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" style="margin-bottom:1.25rem;" onsubmit="return confirm('确定清空当前类型下所有 IP?分组将保留。');">
<input type="hidden" name="action" value="clear_ips_only">
<label for="clear_confirm_phrase">确认清空 IP(在框内输入:<strong>清空IP</strong></label>
<input type="text" id="clear_confirm_phrase" name="clear_confirm_phrase" autocomplete="off" placeholder="清空IP">
<button type="submit" class="danger">清空当前类型全部 IP(保留分组)</button>
</form>
<form method="post" action="<?php echo $self; ?>?<?php echo $typeParam; ?>" onsubmit="return confirm('确定删除当前类型下所有分组及 IP?此操作不可恢复。');">
<input type="hidden" name="action" value="clear_type_all">
<label for="clear_confirm_phrase_all">确认清空全部(在框内输入:<strong>清空全部</strong></label>
<input type="text" id="clear_confirm_phrase_all" name="clear_confirm_phrase_all" autocomplete="off" placeholder="清空全部">
<button type="submit" class="danger">清空当前类型全部分组及 IP</button>
</form>
</section>
<section class="card">
<h2>IP 列表(<?php echo $type === 'white' ? '白名单' : '黑名单'; ?></h2>
<form method="get" action="<?php echo $self; ?>" class="row-inline" style="margin-bottom:12px;align-items:center;">
<input type="hidden" name="type" value="<?php echo h($type); ?>">
<div style="flex:2;min-width:220px;">
<label for="ip_search">搜索 IP(模糊匹配)</label>
<input type="text" id="ip_search" name="ip_search" value="<?php echo h($ipSearch); ?>" placeholder="例如 192.168 或完整 IP">
</div>
<div style="padding-top:1.5rem;">
<button type="submit" class="primary">搜索</button>
<?php if ($ipSearch !== '') { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>">清除</a>
<?php } ?>
</div>
</form>
<p class="muted">共 <?php echo $totalIps; ?> 条,每页 <?php echo $perPage; ?> 条<?php echo $ipSearch !== '' ? '(已筛选)' : ''; ?>。</p>
<?php if ($ipRows === []) { ?>
<p class="muted">当前类型下暂无 IP。</p>
<?php } else { ?>
<table>
<thead>
<tr><th>ID</th><th>IP</th><th>分组</th><th>操作</th></tr>
</thead>
<tbody>
<?php foreach ($ipRows as $row) { ?>
<tr>
<td><?php echo (int) $row['id']; ?></td>
<td><code><?php echo h((string) $row['ip_address']); ?></code></td>
<td><?php echo h((string) $row['group_name']); ?></td>
<td>
<a class="btn-link danger" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page; echo $searchQuerySuffix; ?>&amp;delete_ip=<?php echo (int) $row['id']; ?>"
onclick="return confirm(<?php echo json_encode('确定删除 IP ' . (string) $row['ip_address'] . '', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_UNICODE); ?>);">删除</a>
</td>
</tr>
<?php } ?>
</tbody>
</table>
<?php if ($totalPages > 1) { ?>
<div class="pager">
<?php if ($page > 1) { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page - 1; echo $searchQuerySuffix; ?>">上一页</a>
<?php } ?>
<span>第 <?php echo $page; ?> / <?php echo $totalPages; ?> 页</span>
<?php if ($page < $totalPages) { ?>
<a class="btn-link" href="<?php echo $self; ?>?<?php echo $typeParam; ?>&amp;page=<?php echo $page + 1; echo $searchQuerySuffix; ?>">下一页</a>
<?php } ?>
</div>
<?php } ?>
<?php } ?>
</section>
</body>
</html>
+40
View File
@@ -0,0 +1,40 @@
<?php
define('SHOW_SITE_MODE_SWITCH', 'ip_check' );
define('SHOW_SITE_IP', '' );
define('BLACKLIST_GROUP_ID', 0 );
define('WHITELIST_GROUP_ID', 0 );
define('SHOW_SITE_COUNTRY', 'CN' );
define('CLOAK_SHOW_CONTENT', '302');
define('DB_FP', array (
0 => 'https://www.google.com/',
) );
define('COSTM_IP_SCORE', '20251108_emoUAVF6S' );
define('BLACK_LIST', '0' );
define('AUTO_BLACK', 'OFF' );
define('IS_VIRTUAL', '0' );
define('AUTO_BLACK_TIMES', '5' );
define('WHITE_PARAMS', '' );
define('IPHONE_MODEL', 0 );
define('SHOW_SITE_MOBILE', 0 );
define('SHOW_SITE_OUTPUT', 1 );
define('WRITE_LOG', 1 );
define('CLOAK_ZH_ON', 'OFF');
define('CLOAK_OS_ON', 'OFF');
define('KEEP_PARAMS', 'OFF');
define('CLOAK_REDIRECT_METHOD', 'curl');
define('DB_ZP', 'https://worldhomenest.store');
define('CLOAK_MOBILE_ON', 'OFF');
define('CLOAK_V_REFERER', 'OFF');
define('CLOAK_RISK_NUMBER', '51');
define('CLOAK_RISK_ENHANCED', 'OFF');
define('CLOAK_URL_ARGS_TIMEOUT', '0');
define('CLOAK_AD_FB', 'OFF');
define('CLOAK_AD_GOOGLE', 'ON');
define('CLOAK_AD_TIKTOK', 'ON');
define('CLOAK_AD_STRICT', 'OFF');
define('CLOAK_AD_SPM_ID', 't0tTw4MJ6r');
define('CLOAK_DEBUG_MODE', 'OFF');
?>
Executable
+65
View File
@@ -0,0 +1,65 @@
-- phpMyAdmin SQL Dump
-- version 5.0.4
-- https://www.phpmyadmin.net/
--
-- 主机: localhost
-- 生成日期: 2024-06-25 19:09:38
-- 服务器版本: 5.7.44-log
-- PHP 版本: 7.4.33
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";
--
-- 数据库: `cloak_log`
--
-- --------------------------------------------------------
--
-- 表的结构 `visitor_logs`
--
CREATE TABLE `visitor_logs` (
`id` int(11) UNSIGNED NOT NULL,
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
`visit_md5_code` char(32) NOT NULL DEFAULT 'loss' COMMENT '客户端标记',
`domain` varchar(35) NOT NULL COMMENT '域名',
`visit_date` datetime NOT NULL COMMENT '访问日期',
`IP` varchar(150) NOT NULL COMMENT 'IP',
`country` varchar(10) DEFAULT NULL COMMENT '国家',
`result` char(5) DEFAULT NULL COMMENT '判断结果',
`reason` varchar(100) DEFAULT NULL COMMENT '判断理由',
`referer` text COMMENT '来源',
`vtimes` int(11) UNSIGNED NOT NULL DEFAULT '0' COMMENT '第几次访问',
`client` varchar(20) DEFAULT NULL COMMENT '客户端',
`browser` varchar(20) DEFAULT NULL COMMENT '浏览器',
`device` varchar(300) DEFAULT NULL COMMENT '设备',
`page` text COMMENT '浏览页面',
`fp_url` text COMMENT '跳转页面',
`language` varchar(300) DEFAULT NULL COMMENT '语言',
`add_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '日志记录时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
--
-- 转储表的索引
--
--
-- 表的索引 `visitor_logs`
--
ALTER TABLE `visitor_logs`
ADD PRIMARY KEY (`id`),
ADD KEY `IP` (`IP`);
--
-- 在导出的表使用AUTO_INCREMENT
--
--
-- 使用表AUTO_INCREMENT `visitor_logs`
--
ALTER TABLE `visitor_logs`
MODIFY `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT;
COMMIT;
+11
View File
@@ -0,0 +1,11 @@
{
"folders": [
{
"path": "."
},
{
"path": "../aizheye.test"
}
],
"settings": {}
}
Executable
+173
View File
@@ -0,0 +1,173 @@
<?php
/**
* 二次风控前端指纹脚本入口
*
* 输出分两层(由 CLOAK_RISK_ENHANCED 控制):
* 1. 始终输出(轻量):加载动画 + navigator/screen/时区/自动化检测 + POST f_check.php
* 2. 加强判断 ON 时额外输出(耗时):resources/cloakjs_enhanced.inc.php
* - UAParser、Canvas 指纹、WebGL 深度指纹、WebGPU 基准、字体检测
*/
$__cloak_risk_enhanced = defined('CLOAK_RISK_ENHANCED') && strtoupper(CLOAK_RISK_ENHANCED) === 'ON';
if ($__cloak_risk_enhanced) {
require __DIR__ . '/resources/cloakjs_enhanced.inc.php';
}
?>
<script>
/* ===== 加载动画(轻量,始终输出) ===== */
(function () {
function _0x51ac55() {
var _0x1932aa = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
var _0x5633ca = "";
var _0x5c3c48 = Math.floor(Math.random() * 10) + 4;
for (var _0x53aa2a = 0; _0x53aa2a < _0x5c3c48; _0x53aa2a++) {
_0x5633ca += _0x1932aa.charAt(Math.floor(Math.random() * _0x1932aa.length));
}
return _0x5633ca;
}
var _0x3ead50 = _0x51ac55();
var _0x666e98 = _0x51ac55();
var _0x5e8e1d = _0x51ac55();
var _0x485aab = _0x51ac55();
var _0xd488af = document.createElement("style");
_0xd488af.textContent = "." + _0x666e98 + " {position: relative;width: 40px;}." + _0x666e98 + ":before {content: '';display: block;padding-top: 100%;}." + _0x5e8e1d + " {animation: rotate 2s linear infinite;height: 100%;transform-origin: center center;width: 100%;position: absolute;top: 0;bottom: 0;left: 0;right: 0;margin: auto;}." + _0x485aab + " {stroke-dasharray: 1, 200;stroke-dashoffset: 0;animation: dash 1.5s ease-in-out infinite;stroke-linecap: round;stroke: #535353;stroke-width: 4px;}@keyframes rotate {to { transform: rotate(360deg);}}@keyframes dash {50% { stroke-dasharray: 89, 200; stroke-dashoffset: -35px;}to { stroke-dasharray: 89, 200; stroke-dashoffset: -124px;}}." + _0x3ead50 + " {position: fixed;top: 0;bottom: 0;left: 0;right: 0;z-index: 9999999;background: rgba(255, 255, 255, 255);display: flex;align-items: center;justify-content: center;}.afabcbccfed ." + _0x3ead50 + " { display: none;}";
document.head.appendChild(_0xd488af);
var _0x432f8c = document.createElement("div");
_0x432f8c.className = _0x3ead50;
var _0x9fd2b1 = document.createElement("div");
_0x9fd2b1.className = _0x666e98;
_0x432f8c.appendChild(_0x9fd2b1);
var _0x36c920 = "http://www.w3.org/2000/svg";
var _0x5f00b3 = document.createElementNS(_0x36c920, "svg");
_0x5f00b3.classList.add(_0x5e8e1d);
_0x5f00b3.setAttribute("viewBox", "25 25 50 50");
_0x9fd2b1.appendChild(_0x5f00b3);
var _0x5ccae7 = document.createElementNS(_0x36c920, "circle");
_0x5ccae7.classList.add(_0x485aab);
_0x5ccae7.setAttribute("cx", "50");
_0x5ccae7.setAttribute("cy", "50");
_0x5ccae7.setAttribute("r", "20");
_0x5ccae7.setAttribute("fill", "none");
_0x5ccae7.setAttribute("stroke-width", "2");
_0x5ccae7.setAttribute("stroke-miterlimit", "10");
_0x5f00b3.appendChild(_0x5ccae7);
if (!document.body) {
document.documentElement.appendChild(document.createElement("body"));
}
document.body.insertAdjacentElement("afterbegin", _0x432f8c);
})();
/* ===== 指纹采集 + 提交(轻量字段始终采集;耗时采集仅加强判断 ON) ===== */
(async function () {
const __cloakRiskEnhanced = <?php echo $__cloak_risk_enhanced ? 'true' : 'false'; ?>;
try {
let _0x3b0528;
let _0x12fad4;
window.addEventListener("message", function (_0x3c51e9) {
if (_0x3c51e9.data === "loaded") {
_0x3b0528 = _0x3c51e9.origin;
_0x12fad4 = parent;
}
}, false);
let _0x5aa8d7 = new Map();
_0x5aa8d7.set('isEmulators', false);
if (__cloakRiskEnhanced) {
/* 耗时指纹:Canvas / WebGL 深度 / WebGPU / 字体 / UAParser(定义于 cloakjs_enhanced.inc.php */
await cloakApplyEnhancedFingerprint(_0x5aa8d7);
} else {
/* 加强判断关闭:跳过全部 GPU/Canvas/字体解析,占位 null 供后端识别为基础模式 */
_0x5aa8d7.set("vendor", null);
_0x5aa8d7.set("renderer", null);
_0x5aa8d7.set("canvas", null);
_0x5aa8d7.set("webgl", null);
_0x5aa8d7.set("webGpuData", null);
_0x5aa8d7.set("fonts", []);
_0x5aa8d7.set("browser", null);
}
/* 轻量环境字段(始终采集) */
_0x5aa8d7.set("screen", [window.screen.width, window.screen.height]).set("availScreen", [window.screen.availWidth, window.screen.availHeight]).set("devicePixelRatio", window.devicePixelRatio).set("timezoneOffset", new Date().getTimezoneOffset()).set("webGPU", !!navigator.gpu);
let _0x370f95 = [["userAgent", "userAgent"], ["hardwareConcurrency", "hardwareConcurrency"], ["language", "language"], ["languages", "languages"], ["webdriver", "webdriver"], ["oscpu", "oscpu"], ["deviceMemory", "deviceMemory"], ["platform", "platform"], ["appVersion", "appVersion"]];
new Map(_0x370f95).forEach(function (_0x30f3e1, _0xc7ac9) {
if (_0x30f3e1 in navigator) {
_0x5aa8d7.set(_0xc7ac9, navigator[_0x30f3e1]);
}
});
if ("visualViewport" in window) {
_0x5aa8d7.set("visualViewport", [visualViewport.width, visualViewport.height]);
_0x5aa8d7.set("visualViewportOffset", [visualViewport.offsetLeft, visualViewport.offsetTop]);
_0x5aa8d7.set("windowInnerWidth", window.innerWidth);
_0x5aa8d7.set("scale", visualViewport.scale);
}
if (window.Intl) {
_0x5aa8d7.set("timezone", new window.Intl.DateTimeFormat().resolvedOptions().timeZone);
}
let _0x4e6c16 = 0;
let _0x2b0c77;
if (typeof navigator.maxTouchPoints !== "undefined") {
_0x4e6c16 = navigator.maxTouchPoints;
} else if (typeof navigator.msMaxTouchPoints !== "undefined") {
_0x4e6c16 = navigator.msMaxTouchPoints;
}
try {
document.createEvent("TouchEvent");
_0x2b0c77 = true;
} catch (_0x3035c4) {
_0x2b0c77 = false;
}
let _0x142582 = "ontouchstart" in window;
_0x5aa8d7.set("touchscreen", [_0x4e6c16, _0x2b0c77, _0x142582]);
let _0xae8ab2 = [];
let _0x2b1b36 = () => !!window.RTCPeerConnection || !!window.webkitRTCPeerConnection ? 1 : 0;
_0xae8ab2.push({
isWebRTCSupported: _0x2b1b36()
});
_0x5aa8d7.set("experimental", _0xae8ab2);
let automation = {
isChrome: !!window.chrome,
phantom: !!window.callPhantom || !!window._phantom || false,
selenium: !!document.__selenium_unwrapped__ || !!document.__webdriver_evaluate || false
};
_0x5aa8d7.set('automation', automation);
_0x5aa8d7.set('ip', '<?php echo isset($v_info) && is_object($v_info) ? htmlspecialchars((string) $v_info->v_ip, ENT_QUOTES, 'UTF-8') : ''; ?>');
_0x5aa8d7.set('ciso', '<?php echo isset($v_info) && is_object($v_info) ? htmlspecialchars((string) $v_info->v_country, ENT_QUOTES, 'UTF-8') : ''; ?>');
_0x5aa8d7.set('log_id', '<?php echo (int) ($log_id ?? 0); ?>');
_0x5aa8d7.set('isEmulator', false);
_0x5aa8d7.set('referer', document.referrer);
_0x5aa8d7.set('risk_enhanced', __cloakRiskEnhanced);
let _0x4abcf1 = {};
_0x5aa8d7.forEach((_0x24ebea, _0x3bf077) => {
_0x4abcf1[_0x3bf077] = _0x24ebea;
});
_0x4abcf1['domain'] = window.location.href;
let _0x1120a3 = btoa(JSON.stringify(_0x4abcf1));
let _0x58478a = window.location.search.length > 0 ? decodeURI(window.location.search) : "";
let _0x8edc71 = new XMLHttpRequest();
_0x8edc71.responseType = "json";
_0x8edc71.withCredentials = true;
const apiPath = 'f_check.php';
const targetUrl = new URL(apiPath, window.location.origin).href;
_0x8edc71.open("POST", targetUrl + _0x58478a, true);
_0x8edc71.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
_0x8edc71.send("hdata=" + _0x1120a3);
_0x8edc71.addEventListener("readystatechange", () => {
if (_0x8edc71.readyState === 4 && _0x8edc71.status === 200) {
let link = _0x8edc71.response.link;
if (link) {
window.location.href=link;
} else {
document.body.classList.add("afabcbccfed");
}
}
});
} catch (_0xcloakErr) {
console.error('cloak fingerprint failed', _0xcloakErr);
}
})();
</script>
+69
View File
@@ -0,0 +1,69 @@
<?php
/**
* 加载站点 check_config 配置(define 常量)
*/
class ConfigLoader
{
public static function loadFile($configFile)
{
if (file_exists($configFile)) {
include $configFile;
}
}
/**
* @return string 实际加载的配置名
*/
public static function loadByName($configName, $baseDir = null)
{
$baseDir = $baseDir ?: dirname(__DIR__);
$configName = self::sanitizeConfigName($configName);
$configFile = $baseDir . '/check_config/' . $configName . '_config.php';
if (file_exists($configFile)) {
self::loadFile($configFile);
return $configName;
}
self::loadFile($baseDir . '/check_config/index_config.php');
return 'index';
}
/**
* @return string 实际加载的配置名
*/
public static function loadByHost($host, $baseDir = null)
{
$baseDir = $baseDir ?: dirname(__DIR__);
if (!class_exists('DomainRepository', false)) {
require_once dirname(__DIR__) . '/lib/Cloak/DomainRepository.php';
}
$name = DomainRepository::resolveConfigNameForHost((string) $host, $baseDir);
return self::loadByName($name, $baseDir);
}
/**
* 优先 HTTP Host(多站/宝塔),否则入口脚本名(单站兼容)
*
* @return string 实际加载的配置名
*/
public static function loadForRequest($scriptBaseName, $host = null)
{
$host = $host ?? ($_SERVER['HTTP_HOST'] ?? '');
if ($host !== '') {
return self::loadByHost($host);
}
if ($scriptBaseName !== null && $scriptBaseName !== '') {
return self::loadByName($scriptBaseName);
}
return self::loadByName('index');
}
public static function sanitizeConfigName($name)
{
$name = trim((string) $name);
$name = str_replace('.php', '', $name);
if ($name === '' || !preg_match('/^[a-zA-Z0-9_-]+$/', $name)) {
return 'index';
}
return $name;
}
}
Executable
+28
View File
@@ -0,0 +1,28 @@
<?php
// error_reporting(E_ALL);
// ini_set('display_errors', 1);
define('LOGIN_PASSWORD', 'test123456');
define('DB_USERNAME', 'cloaka_test');
define('DB_NAME', 'cloaka_test');
define('DB_PASSWORD', 'hNeb3phx3QX12fGC');
// --- API 鉴权秘钥(全局通用,对接 tiktokba.com cloak 服务)---
define('CHECK_KEY', 'V6eISi8BsQNua3579ZzTbRwxmlXoJ4MF');
// --- Redis 通用配置(可选)---
define('REDIS_ENABLED', 'ON');
define('REDIS_HOST', '127.0.0.1');
define('REDIS_PORT', 6379);
define('REDIS_TIMEOUT', 1);
define('REDIS_PASSWORD', '');
// --- 访客日志异步写入(ON=队列落库,OFF=同步 INSERT---
define('VISITOR_LOG_ASYNC', 'ON');
// --- Cloudflare(可选;留空则域名仅本地登记,不走 API)---
define('SERVER_IP', '');
define('CLOUDFLARE_API_TOKEN', '');
define('CLOUDFLARE_ACCOUNT_ID', '');
Executable
+20
View File
@@ -0,0 +1,20 @@
<?php
// error_reporting(E_ALL);
// ini_set('display_errors', 1);
define('LOGIN_PASSWORD', 'test123456');
define('DB_USERNAME', 'cloaka_test');
define('DB_NAME', 'cloaka_test');
define('DB_PASSWORD', 'hNeb3phx3QX12fGC');
// --- API 鉴权秘钥(全局通用,对接 tiktokba.com cloak 服务)---
define('CHECK_KEY', '');
// --- Redis 通用配置(供 CloakTrafficGuard 防重放功能使用)---
define('REDIS_ENABLED', 'ON');
define('REDIS_HOST', '127.0.0.1');
define('REDIS_PORT', 6379);
define('REDIS_TIMEOUT', 0.5);
define('REDIS_PASSWORD', '');
Executable
+1187
View File
File diff suppressed because it is too large Load Diff
+250
View File
@@ -0,0 +1,250 @@
<?php
// 国家全程与简码
$countries = array(
"XXX"=> "",
"AF" => "Afghanistan",
"AX" => "Aland Islands",
"AL" => "Albania",
"DZ" => "Algeria",
"AS" => "American Samoa",
"AD" => "Andorra",
"AO" => "Angola",
"AI" => "Anguilla",
"AQ" => "Antarctica",
"AG" => "Antigua and Barbuda",
"AR" => "Argentina",
"AM" => "Armenia",
"AW" => "Aruba",
"AU" => "Australia",
"AT" => "Austria",
"AZ" => "Azerbaijan",
"BS" => "Bahamas",
"BH" => "Bahrain",
"BD" => "Bangladesh",
"BB" => "Barbados",
"BY" => "Belarus",
"BE" => "Belgium",
"BZ" => "Belize",
"BJ" => "Benin",
"BM" => "Bermuda",
"BT" => "Bhutan",
"BO" => "Bolivia, Plurinational State of",
"BQ" => "Bonaire, Sint Eustatius and Saba",
"BA" => "Bosnia and Herzegovina",
"BW" => "Botswana",
"BV" => "Bouvet Island",
"BR" => "Brazil",
"IO" => "British Indian Ocean Territory",
"BN" => "Brunei Darussalam",
"BG" => "Bulgaria",
"BF" => "Burkina Faso",
"BI" => "Burundi",
"CV" => "Cabo Verde",
"KH" => "Cambodia",
"CM" => "Cameroon",
"CA" => "Canada",
"KY" => "Cayman Islands",
"CF" => "Central African Republic",
"TD" => "Chad",
"CL" => "Chile",
"CN" => "China",
"CC" => "Cocos (Keeling) Islands",
"CO" => "Colombia",
"KM" => "Comoros",
"CG" => "Congo",
"CD" => "Congo, The Democratic Republic of The",
"CK" => "Cook Islands",
"CR" => "Costa Rica",
"CI" => "Cote D'ivoire",
"HR" => "Croatia",
"CU" => "Cuba",
"CW" => "Curacao",
"CY" => "Cyprus",
"CZ" => "Czech Republic",
"DK" => "Denmark",
"DJ" => "Djibouti",
"DM" => "Dominica",
"DO" => "Dominican Republic",
"EC" => "Ecuador",
"EG" => "Egypt",
"SV" => "El Salvador",
"GQ" => "Equatorial Guinea",
"ER" => "Eritrea",
"EE" => "Estonia",
"ET" => "Ethiopia",
"FK" => "Falkland Islands (Malvinas)",
"FO" => "Faroe Islands",
"FJ" => "Fiji",
"FI" => "Finland",
"FR" => "France",
"GF" => "French Guiana",
"PF" => "French Polynesia",
"TF" => "French Southern Territories",
"GA" => "Gabon",
"GM" => "Gambia",
"GE" => "Georgia",
"DE" => "Germany",
"GH" => "Ghana",
"GI" => "Gibraltar",
"GR" => "Greece",
"GL" => "Greenland",
"GD" => "Grenada",
"GP" => "Guadeloupe",
"GU" => "Guam",
"GT" => "Guatemala",
"GG" => "Guernsey",
"GN" => "Guinea",
"GW" => "Guinea-Bissau",
"GY" => "Guyana",
"HT" => "Haiti",
"VA" => "Holy See",
"HN" => "Honduras",
"HK" => "Hong Kong",
"HU" => "Hungary",
"IS" => "Iceland",
"IN" => "India",
"ID" => "Indonesia",
"IR" => "Iran, Islamic Republic of",
"IQ" => "Iraq",
"IE" => "Ireland",
"IM" => "Isle of Man",
"IL" => "Israel",
"IT" => "Italy",
"JM" => "Jamaica",
"JP" => "Japan",
"JE" => "Jersey",
"JO" => "Jordan",
"KZ" => "Kazakhstan",
"KE" => "Kenya",
"KI" => "Kiribati",
"KP" => "Korea, Democratic People's Republic of",
"KR" => "Korea, Republic of",
"KW" => "Kuwait",
"KG" => "Kyrgyzstan",
"LA" => "Lao People's Democratic Republic",
"LV" => "Latvia",
"LB" => "Lebanon",
"LS" => "Lesotho",
"LR" => "Liberia",
"LY" => "Libya",
"LI" => "Liechtenstein",
"LT" => "Lithuania",
"LU" => "Luxembourg",
"MO" => "Macao",
"MK" => "Macedonia, The Former Yugoslav Republic of",
"MG" => "Madagascar",
"MW" => "Malawi",
"MY" => "Malaysia",
"MV" => "Maldives",
"ML" => "Mali",
"MT" => "Malta",
"MH" => "Marshall Islands",
"MQ" => "Martinique",
"MR" => "Mauritania",
"MU" => "Mauritius",
"YT" => "Mayotte",
"MX" => "Mexico",
"FM" => "Micronesia, Federated States of",
"MD" => "Moldova, Republic of",
"MC" => "Monaco",
"MN" => "Mongolia",
"ME" => "Montenegro",
"MS" => "Montserrat",
"MA" => "Morocco",
"MZ" => "Mozambique",
"MM" => "Myanmar",
"NA" => "Namibia",
"NR" => "Nauru",
"NP" => "Nepal",
"NL" => "Netherlands",
"NC" => "New Caledonia",
"NZ" => "New Zealand",
"NI" => "Nicaragua",
"NE" => "Niger",
"NG" => "Nigeria",
"NU" => "Niue",
"NF" => "Norfolk Island",
"MP" => "Northern Mariana Islands",
"NO" => "Norway",
"OM" => "Oman",
"PK" => "Pakistan",
"PW" => "Palau",
"PS" => "Palestine, State of",
"PA" => "Panama",
"PG" => "Papua New Guinea",
"PY" => "Paraguay",
"PE" => "Peru",
"PH" => "Philippines",
"PN" => "Pitcairn",
"PL" => "Poland",
"PT" => "Portugal",
"PR" => "Puerto Rico",
"QA" => "Qatar",
"RE" => "Reunion",
"RO" => "Romania",
"RU" => "Russian Federation",
"RW" => "Rwanda",
"BL" => "Saint Barthelemy",
"KN" => "Saint Kitts and Nevis",
"LC" => "Saint Lucia",
"MF" => "Saint Martin (French Part)",
"PM" => "Saint Pierre and Miquelon",
"VC" => "Saint Vincent and The Grenadines",
"WS" => "Samoa",
"SM" => "San Marino",
"ST" => "Sao Tome and Principe",
"SA" => "Saudi Arabia",
"SN" => "Senegal",
"RS" => "Serbia",
"SC" => "Seychelles",
"SL" => "Sierra Leone",
"SG" => "Singapore",
"SX" => "Sint Maarten (Dutch Part)",
"SK" => "Slovakia",
"SI" => "Slovenia",
"SB" => "Solomon Islands",
"SO" => "Somalia",
"ZA" => "South Africa",
"GS" => "South Georgia and The South Sandwich Islands",
"SS" => "South Sudan",
"ES" => "Spain",
"LK" => "Sri Lanka",
"SD" => "Sudan",
"SR" => "Suriname",
"SJ" => "Svalbard and Jan Mayen",
"SZ" => "Swaziland",
"SE" => "Sweden",
"CH" => "Switzerland",
"SY" => "Syrian Arab Republic",
"TW" => "Taiwan, Province of China",
"TJ" => "Tajikistan",
"TZ" => "Tanzania, United Republic of",
"TH" => "Thailand",
"TL" => "Timor-Leste",
"TG" => "Togo",
"TK" => "Tokelau",
"TO" => "Tonga",
"TT" => "Trinidad and Tobago",
"TN" => "Tunisia",
"TR" => "Turkey",
"TM" => "Turkmenistan",
"TC" => "Turks and Caicos Islands",
"TV" => "Tuvalu",
"UG" => "Uganda",
"UA" => "Ukraine",
"AE" => "United Arab Emirates",
"GB" => "United Kingdom",
"US" => "United States",
"UM" => "United States Minor Outlying Islands",
"UY" => "Uruguay",
"UZ" => "Uzbekistan",
"VU" => "Vanuatu",
"VE" => "Venezuela, Bolivarian Republic of",
"VN" => "Viet Nam",
"VG" => "Virgin Islands, British",
"VI" => "Virgin Islands, U.S.",
"WF" => "Wallis and Futuna",
"YE" => "Yemen",
"ZM" => "Zambia",
"ZW" => "Zimbabwe",
);
+172
View File
@@ -0,0 +1,172 @@
<?php
require_once(dirname(__DIR__) . "/datatable/countries.php");
require_once(dirname(__DIR__) . "/cong.php");
require_once(dirname(__DIR__) . "/lib/DbHelper.php");
require_once(dirname(__DIR__) . "/lib/Cloak/PipelineTimer.php");
require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogSchema.php");
session_start();
$reasons = array(
"blank_referrer" => "过滤无来源的访问",
"blacklisted_ptr" => "过滤黑名单主机名的访问者",
"proxy" => " 过滤代理和VPN",
"filter_isps" => "过滤不常见的服务提供商",
"switched_browsers" => "过滤多少分钟内切换浏览器",
"invalid_google_click_id" => "必须是有效的googleID",
"non_touch_device" => "过滤非接触设备",
"spoofed_browser" => "过滤浏览器欺骗设备",
"sticky_filtering" => "启用粘贴过滤",
"whitelisted_browser_ids" => "可访问的用户ID",
"blacklisted_browser_ids" => "不可访问的用户ID",
"whitelisted_urlrules" => "白名单链接参数",
"blacklisted_urlrules" => "黑名单链接参数",
"device_desktop" => "过滤电脑访问",
"device_mobile" => "过滤手机的访问",
"device_tablet" => "过滤平板的访问",
"device_no_accel" => "过滤没有加速度计/陀螺仪的设备",
"device_headless_browser" => "过滤没有浏览器头的访问",
"isp_type" => "过滤指定的ISP类型",
"browser_time_zone" => "可以访问的浏览器时区",
"whitelisted_language_codes" => "允许访问的浏览器语言",
"blacklisted_language_codes" => "不允许访问的浏览器语言",
"agent" => "代理商列表",
"ip_lists" => "IP列表",
"referrer" => "来源用户列表",
"org" => "组织机构列表",
"deadbolt" => "是否限制访问",
"global_db" => "全球IP数据库",
"org_isp" => "组织或者ISP",
"cross_campaign_ip_visits" => "每个IP最多可访问多少个",
"too_many_ip_visits" => "每个IP最多可访问多少次",
"too_many_browser_visits" => "每个浏览器最多可访问多少次",
"whitelisted_safe_page_url" => "仅允许页面地址包含",
"blacklisted_safe_page_url" => "过滤页面地址包含",
"time_of_day" => "不可访问的日期",
);
if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
die("404 Not Found");
}
$servername = "localhost";
$username = DB_USERNAME;
$password = DB_PASSWORD;
$dbname = DB_NAME;
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
$jsonResult = array();
// Check connection
if ($conn->connect_error) {
$error = "Connection failed: " . $conn->connect_error;
$handle = fopen(dirname(__FILE__) . "/err.txt", "a+");
fwrite($handle, date("Y-m-d H:i:s") . ' | ' . $error . "\n");
fclose($handle);
$jsonResult["code"] = 1;
$jsonResult["msg"] = $error;
$jsonResult["count"] = 0;
$jsonResult["data"] = array();
} else {
VisitorLogSchema::ensureColumns($conn);
if(isset($_GET['clear']) && $_GET['clear'] == "true") { //清除日志数据表
$sql = "TRUNCATE `visitor_logs`";
// 执行查询并输出结果
$result = mysqli_query($conn, $sql);
$jsonResult["code"] = 0;
$jsonResult["msg"] = "success";
$jsonResult["count"] = 0;
$jsonResult["data"] = array();
} else { // 默认分页数据
// 获取当前页码(默认为第1页)
$pageNumber = isset($_GET['page']) ? (int) $_GET['page'] : 1;
// 设置每页显示的记录条数
$recordsPerPage = isset($_GET['limit']) ? (int) $_GET['limit'] : 50;
// 计算起始位置
$startPosition = ($pageNumber - 1) * $recordsPerPage;
$map = array();
$where = 1;
if(!empty($_GET['visit_date'])) {
$map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'";
}
$where .= cloak_visitor_log_apply_result_filter($conn, $_GET['result'] ?? '');
if(!empty($_GET['country'])) {
$map[] = "country LIKE '" . cloak_db_escape($conn, $_GET['country']) . "'";
}
if(!empty($_GET['IP'])) {
$map[] = "IP LIKE '" . cloak_db_escape($conn, $_GET['IP']) . "'";
}
if(!empty($_GET['domain'])) {
$map[] = "domain LIKE '%" . cloak_db_escape($conn, $_GET['domain']) . "%'";
}
if(!empty($_GET['spage'])) {
$map[] = "page LIKE '%" . cloak_db_escape($conn, $_GET['spage']) . "%'";
}
if(!empty($map)) {
$where .= " AND " . join(" AND ", $map);
}
// SQL语句
$sql = "SELECT `id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `device`, `add_time` FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}";
// 执行查询并输出结果
$result = mysqli_query($conn, $sql);
$jsonData = array();
while ($row = mysqli_fetch_assoc($result)) {
//echo "<p>{$row["columnName"]}</p>"; // 根据需要修改列名
if($row['vtimes'] == 0) {
$row['vtimes'] = 1; // 还未统计访问次数
$sqlVtimes = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE `IP` = '" . cloak_db_escape($conn, $row["IP"]) . "' AND `domain` = '" . cloak_db_escape($conn, $row["domain"]) . "' AND id < " . (int)$row['id']);
$vtimesRow = mysqli_fetch_assoc($sqlVtimes);
$vtimes = $vtimesRow["total"]; // 如果之前此IP有访问记录
if($vtimes > 0) {
$row['vtimes'] += $vtimes; // 访问次数叠加
}
// 更新访问次数
$updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id'];
mysqli_query($conn, $updateSql);
}
if(!empty($row['country'])) {
$row['country'] = isset($countries[$row['country']])?$countries[$row['country']]:"";
} else {
$row['country'] = "";
}
if(isset($reasons[$row['reason']])) {
$row['reason'] = $reasons[$row['reason']];
}
if ($row['result'] === 'wait') {
$row['result'] = '检测中';
}
$row['judge_timing_text'] = cloak_visitor_log_format_timing($row['judge_timing'] ?? '');
$jsonData[] = $row;
}
$sqlCount = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE {$where}");
$countRow = mysqli_fetch_assoc($sqlCount);
$jsonResult["code"] = 0;
$jsonResult["msg"] = "";
$jsonResult["count"] = $countRow['total'];
$jsonResult["data"] = $jsonData;
}
}
// 关闭数据库连接
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode($jsonResult);exit;
+3
View File
@@ -0,0 +1,3 @@
2023-12-31 18:29:49 | Connection failed: Access denied for user 'root'@'localhost' (using password: YES)
2023-12-31 18:31:01 | Connection failed: Access denied for user 'root'@'localhost' (using password: YES)
2023-12-31 18:31:10 | Connection failed: Access denied for user 'root'@'localhost' (using password: YES)
+127
View File
@@ -0,0 +1,127 @@
<?php
set_time_limit(0);
header('Content-Type: text/csv; charset=UTF-8');
header('Content-Disposition: attachment; filename="export_'.date("Ymd").'.csv"');
require_once(dirname(__DIR__) . "/datatable/countries.php");
require_once(dirname(__DIR__) . "/cong.php");
require_once(dirname(__DIR__) . "/lib/DbHelper.php");
require_once(dirname(__DIR__) . "/lib/Cloak/PipelineTimer.php");
require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogSchema.php");
session_start();
if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
die("404 Not Found");
}
$reasons = array(
"blank_referrer" => "过滤无来源的访问",
"blacklisted_ptr" => "过滤黑名单主机名的访问者",
"proxy" => " 过滤代理和VPN",
"filter_isps" => "过滤不常见的服务提供商",
"switched_browsers" => "过滤多少分钟内切换浏览器",
"invalid_google_click_id" => "必须是有效的googleID",
"non_touch_device" => "过滤非接触设备",
"spoofed_browser" => "过滤浏览器欺骗设备",
"sticky_filtering" => "启用粘贴过滤",
"whitelisted_browser_ids" => "可访问的用户ID",
"blacklisted_browser_ids" => "不可访问的用户ID",
"whitelisted_urlrules" => "白名单链接参数",
"blacklisted_urlrules" => "黑名单链接参数",
"device_desktop" => "过滤电脑访问",
"device_mobile" => "过滤手机的访问",
"device_tablet" => "过滤平板的访问",
"device_no_accel" => "过滤没有加速度计/陀螺仪的设备",
"device_headless_browser" => "过滤没有浏览器头的访问",
"isp_type" => "过滤指定的ISP类型",
"browser_time_zone" => "可以访问的浏览器时区",
"whitelisted_language_codes" => "允许访问的浏览器语言",
"blacklisted_language_codes" => "不允许访问的浏览器语言",
"agent" => "代理商列表",
"ip_lists" => "IP列表",
"referrer" => "来源用户列表",
"org" => "组织机构列表",
"deadbolt" => "是否限制访问",
"global_db" => "全球IP数据库",
"org_isp" => "组织或者ISP",
"cross_campaign_ip_visits" => "每个IP最多可访问多少个",
"too_many_ip_visits" => "每个IP最多可访问多少次",
"too_many_browser_visits" => "每个浏览器最多可访问多少次",
"whitelisted_safe_page_url" => "仅允许页面地址包含",
"blacklisted_safe_page_url" => "过滤页面地址包含",
"time_of_day" => "不可访问的日期",
);
$servername = "localhost";
$username = DB_USERNAME;
$password = DB_PASSWORD;
$dbname = DB_NAME;
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
$error = "Connection failed: " . $conn->connect_error;
$handle = fopen(dirname(__FILE__) . "/err.txt", "a+");
fwrite($handle, date("Y-m-d H:i:s") . ' | ' . $error . "\n");
fclose($handle);
echo "数据库连接失败";exit;
} else {
VisitorLogSchema::ensureColumns($conn);
$map = array();
$where = 1;
if(!empty($_GET['visit_date'])) {
$map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'";
}
$where .= cloak_visitor_log_apply_result_filter($conn, $_GET['result'] ?? '');
if(!empty($_GET['country'])) {
$map[] = "country LIKE '" . cloak_db_escape($conn, $_GET['country']) . "'";
}
if(!empty($_GET['IP'])) {
$map[] = "IP LIKE '" . cloak_db_escape($conn, $_GET['IP']) . "'";
}
if(!empty($_GET['domain'])) {
$map[] = "domain LIKE '%" . cloak_db_escape($conn, $_GET['domain']) . "%'";
}
if(!empty($_GET['spage'])) {
$map[] = "page LIKE '%" . cloak_db_escape($conn, $_GET['spage']) . "%'";
}
if(!empty($map)) {
$where .= " AND " . join(" AND ", $map);
}
// SQL语句
$sql = "SELECT `id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `device`, `add_time` FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC";
// 执行查询并输出结果
$result = mysqli_query($conn, $sql);
echo "\xEF\xBB\xBF";
$fp = fopen('php://output', 'w');
while ($row = mysqli_fetch_assoc($result)) {
$row['country'] = $countries[$row['country']];
if(isset($reasons[$row['reason']])) {
$row['reason'] = $reasons[$row['reason']];
}
if ($row['result'] === 'wait') {
$row['result'] = '检测中';
}
$row['judge_timing_text'] = cloak_visitor_log_format_timing($row['judge_timing'] ?? '');
fputcsv($fp, $row);
}
fclose($fp);
exit("");
}
+36
View File
@@ -0,0 +1,36 @@
<?php
require_once dirname(__DIR__) . '/cong.php';
require_once dirname(__DIR__) . '/lib/DbHelper.php';
require_once dirname(__DIR__) . '/lib/Cloak/VisitorLogAnalytics.php';
session_start();
header('Content-Type: application/json; charset=utf-8');
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
http_response_code(403);
echo json_encode(['code' => 1, 'msg' => '未授权', 'data' => null], JSON_UNESCAPED_UNICODE);
exit;
}
$conn = new mysqli('localhost', DB_USERNAME, DB_PASSWORD, DB_NAME);
if ($conn->connect_error) {
echo json_encode(['code' => 1, 'msg' => '数据库连接失败', 'data' => null], JSON_UNESCAPED_UNICODE);
exit;
}
$filters = [
'date_from' => $_GET['date_from'] ?? '',
'date_to' => $_GET['date_to'] ?? '',
'domain' => $_GET['domain'] ?? '',
'campagin_id' => $_GET['campagin_id'] ?? '',
];
$data = VisitorLogAnalytics::buildReport($conn, $filters);
$conn->close();
echo json_encode([
'code' => 0,
'msg' => 'success',
'data' => $data,
], JSON_UNESCAPED_UNICODE);
+162
View File
@@ -0,0 +1,162 @@
<?php
require_once(dirname(__DIR__) . "/datatable/countries.php");
require_once(dirname(__DIR__) . "/cong.php");
require_once(dirname(__DIR__) . "/lib/DbHelper.php");
require_once(dirname(__DIR__) . "/lib/Cloak/PipelineTimer.php");
require_once(dirname(__DIR__) . "/lib/Cloak/VisitorLogSchema.php");
$reasons = array(
"blank_referrer" => "过滤无来源的访问",
"blacklisted_ptr" => "过滤黑名单主机名的访问者",
"proxy" => " 过滤代理和VPN",
"filter_isps" => "过滤不常见的服务提供商",
"switched_browsers" => "过滤多少分钟内切换浏览器",
"invalid_google_click_id" => "必须是有效的googleID",
"non_touch_device" => "过滤非接触设备",
"spoofed_browser" => "过滤浏览器欺骗设备",
"sticky_filtering" => "启用粘贴过滤",
"whitelisted_browser_ids" => "可访问的用户ID",
"blacklisted_browser_ids" => "不可访问的用户ID",
"whitelisted_urlrules" => "白名单链接参数",
"blacklisted_urlrules" => "黑名单链接参数",
"device_desktop" => "过滤电脑访问",
"device_mobile" => "过滤手机的访问",
"device_tablet" => "过滤平板的访问",
"device_no_accel" => "过滤没有加速度计/陀螺仪的设备",
"device_headless_browser" => "过滤没有浏览器头的访问",
"isp_type" => "过滤指定的ISP类型",
"browser_time_zone" => "可以访问的浏览器时区",
"whitelisted_language_codes" => "允许访问的浏览器语言",
"blacklisted_language_codes" => "不允许访问的浏览器语言",
"agent" => "代理商列表",
"ip_lists" => "IP列表",
"referrer" => "来源用户列表",
"org" => "组织机构列表",
"deadbolt" => "是否限制访问",
"global_db" => "全球IP数据库",
"org_isp" => "组织或者ISP",
"cross_campaign_ip_visits" => "每个IP最多可访问多少个",
"too_many_ip_visits" => "每个IP最多可访问多少次",
"too_many_browser_visits" => "每个浏览器最多可访问多少次",
"whitelisted_safe_page_url" => "仅允许页面地址包含",
"blacklisted_safe_page_url" => "过滤页面地址包含",
"time_of_day" => "不可访问的日期",
);
$servername = "localhost";
$username = DB_USERNAME;
$password = DB_PASSWORD;
$dbname = DB_NAME;
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
$jsonResult = array();
// Check connection
if ($conn->connect_error) {
$error = "Connection failed: " . $conn->connect_error;
$handle = fopen(dirname(__FILE__) . "/err.txt", "a+");
fwrite($handle, date("Y-m-d H:i:s") . ' | ' . $error . "\n");
fclose($handle);
$jsonResult["code"] = 1;
$jsonResult["msg"] = $error;
$jsonResult["count"] = 0;
$jsonResult["data"] = array();
} else {
VisitorLogSchema::ensureColumns($conn);
if(isset($_GET['clear']) && $_GET['clear'] == "true") { //清除日志数据表
$sql = "TRUNCATE `visitor_logs`";
// 执行查询并输出结果
$result = mysqli_query($conn, $sql);
$jsonResult["code"] = 0;
$jsonResult["msg"] = "success";
$jsonResult["count"] = 0;
$jsonResult["data"] = array();
} else { // 默认分页数据
// 获取当前页码(默认为第1页)
$pageNumber = isset($_GET['page']) ? (int) $_GET['page'] : 1;
// 设置每页显示的记录条数
$recordsPerPage = isset($_GET['limit']) ? (int) $_GET['limit'] : 50;
// 计算起始位置
$startPosition = ($pageNumber - 1) * $recordsPerPage;
$map = array();
$where = 1;
if(!empty($_GET['visit_date'])) {
$map[] = "DATE_FORMAT(`visit_date`, '%Y-%m-%d') LIKE '" . cloak_db_escape($conn, $_GET['visit_date']) . "'";
}
$where .= cloak_visitor_log_apply_result_filter($conn, $_GET['result'] ?? '');
if(!empty($_GET['country'])) {
$map[] = "country LIKE '" . cloak_db_escape($conn, $_GET['country']) . "'";
}
if(!empty($_GET['IP'])) {
$map[] = "IP LIKE '" . cloak_db_escape($conn, $_GET['IP']) . "'";
}
if(!empty($_GET['domain'])) {
$map[] = "domain LIKE '%" . cloak_db_escape($conn, $_GET['domain']) . "%'";
}
if(!empty($_GET['spage'])) {
$map[] = "page LIKE '%" . cloak_db_escape($conn, $_GET['spage']) . "%'";
}
if(!empty($map)) {
$where .= " AND " . join(" AND ", $map);
}
// SQL语句
$sql = "SELECT `id`, `campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `fp_url`, `referer`, `vtimes`, `client`, `browser`, `page`, `language`, `user_agent`, `http_referer`, `accept_language_raw`, `judge_timing`, `device`, `add_time` FROM `visitor_logs` WHERE {$where} ORDER BY `id` DESC LIMIT {$startPosition}, {$recordsPerPage}";
// 执行查询并输出结果
$result = mysqli_query($conn, $sql);
$jsonData = array();
while ($row = mysqli_fetch_assoc($result)) {
//echo "<p>{$row["columnName"]}</p>"; // 根据需要修改列名
if($row['vtimes'] == 0) {
$row['vtimes'] = 1; // 还未统计访问次数
$sqlVtimes = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE `IP` = '" . cloak_db_escape($conn, $row["IP"]) . "' AND `domain` = '" . cloak_db_escape($conn, $row["domain"]) . "' AND id < " . (int)$row['id']);
$vtimesRow = mysqli_fetch_assoc($sqlVtimes);
$vtimes = $vtimesRow["total"]; // 如果之前此IP有访问记录
if($vtimes > 0) {
$row['vtimes'] += $vtimes; // 访问次数叠加
}
// 更新访问次数
$updateSql = "UPDATE `visitor_logs` SET `vtimes`=" . (int)$row['vtimes'] . " WHERE id = " . $row['id'];
mysqli_query($conn, $updateSql);
}
$row['country'] = $countries[$row['country']];
if(isset($reasons[$row['reason']])) {
$row['reason'] = $reasons[$row['reason']];
}
if ($row['result'] === 'wait') {
$row['result'] = '检测中';
}
$row['judge_timing_text'] = cloak_visitor_log_format_timing($row['judge_timing'] ?? '');
$jsonData[] = $row;
}
$sqlCount = mysqli_query($conn, "SELECT count(*) as `total` FROM `visitor_logs` WHERE {$where}");
$countRow = mysqli_fetch_assoc($sqlCount);
$jsonResult["code"] = 0;
$jsonResult["msg"] = "";
$jsonResult["count"] = $countRow['total'];
$jsonResult["data"] = $jsonData;
}
}
// 关闭数据库连接
mysqli_close($conn);
header('Content-Type: application/json');
echo json_encode($jsonResult);exit;
+90
View File
@@ -0,0 +1,90 @@
# IPCLOAKAPI 宝塔一键部署说明
## 适用场景
在一台宝塔服务器上**安装一次**,之后新建的 PHP 网站可通过钩子自动接入 Cloak 流量判定(需在后台绑定域名与配置)。
## 三步安装(客户向)
### 第 1 步:上传并执行安装脚本
1. 将整个项目上传到服务器(或通过 Git 拉取)。
2. SSH 登录服务器,执行:
```bash
cd /path/to/cloaka
sudo bash deploy/baota/install_bt.sh
```
3. 按提示输入**管理后台域名**(例如 `cloak-admin.你的域名.com`)。脚本会把程序安装到 `/www/cloaka`(可改 `--root`)。
### 第 2 步:完成 Web 安装向导
浏览器打开:
```text
https://cloak-admin.你的域名.com/install.php
```
填写 MySQL、后台密码、API 秘钥(CHECK_KEY),点击安装。
### 第 3 步:为每个业务站配置
对每个投流域名:
1. **先在宝塔**:网站 → 添加站点 → 填写域名 → 申请 **SSL**(必须)。
2. **再在后台**`dashboard.php`**站点域名** → 添加相同域名。
3. **新建或编辑配置****关联域名** 选择该域名 → 填写仿品/正品链接 → 保存。
> 未添加域名时,广告链接默认使用**当前访问后台的域名**。
## 新建宝塔网站时会发生什么
安装脚本会在 `/www/server/panel/hook/` 注册 `cloaka_post_site.sh`
- 为新站点 Nginx 配置追加 Cloak 路由片段(请求走 `/www/cloaka/ip_check.php`)。
- 尝试在数据库 `cloak_site_domains` 登记该域名(需已完成 install.php)。
## 手动绑定已有站点
编辑站点 Nginx 配置,在 `server { }` 内追加(注意修改 PHP 版本路径):
```nginx
# BEGIN IPCLOAKAPI
# 内容见 /www/cloaka/deploy/baota/nginx/cloaka-site.conf.installed
# END IPCLOAKAPI
```
或重新运行:
```bash
sudo CLOAK_ROOT=/www/cloaka bash /www/cloaka/deploy/baota/hook/post_site.sh 站点域名 /www/wwwroot/站点域名
```
## 升级
```bash
sudo bash deploy/baota/install_bt.sh --upgrade --source /path/to/new/cloaka
```
不会覆盖 `cong.php``.install.lock``check_config/*_config.php`
## 目录说明
| 路径 | 说明 |
|------|------|
| `/www/cloaka` | 程序主目录(默认) |
| `deploy/baota/nginx/cloaka-site.conf` | 业务站 Nginx 模板 |
| `deploy/baota/nginx/cloaka-admin.conf` | 管理站 Nginx 补充 |
| `deploy/baota/hook/post_site.sh` | 建站后钩子 |
## 常见问题
**Q:访问业务站 502**
检查 `cloaka-site.conf.installed` 中 PHP include 路径是否与站点 PHP 版本一致(安装脚本会按 82/83/81 检测)。
**Q:配置不生效?**
确认后台已为该域名**关联配置**,且 `check_config/配置名_config.php` 存在。
**Q:广告链接域名不对?**
在编辑配置中选择正确的「关联域名」;未选择时使用后台当前访问域名。
+55
View File
@@ -0,0 +1,55 @@
#!/bin/bash
# 宝塔建站完成后钩子:为站点启用 Cloak 路由
# 用法:post_site.sh <域名> <网站根目录>
set -euo pipefail
if [[ -f /etc/cloaka.env ]]; then
# shellcheck disable=SC1091
source /etc/cloaka.env
fi
CLOAK_ROOT="${CLOAK_ROOT:-/www/cloaka}"
SITE_DOMAIN="${1:-}"
SITE_PATH="${2:-}"
MARK_BEGIN="# BEGIN IPCLOAKAPI"
MARK_END="# END IPCLOAKAPI"
[[ -z "$SITE_DOMAIN" ]] && exit 0
VHOST="/www/server/panel/vhost/nginx/${SITE_DOMAIN}.conf"
SNIPPET_INST="${CLOAK_ROOT}/deploy/baota/nginx/cloaka-site.conf.installed"
if [[ ! -f "$VHOST" ]]; then
exit 0
fi
if [[ ! -f "$SNIPPET_INST" ]]; then
exit 0
fi
if ! grep -qF "$MARK_BEGIN" "$VHOST" 2>/dev/null; then
{
echo ""
echo "$MARK_BEGIN"
cat "$SNIPPET_INST"
echo "$MARK_END"
} >> "$VHOST"
fi
if [[ -f "${CLOAK_ROOT}/cong.php" ]]; then
php -r "
require '${CLOAK_ROOT}/cong.php';
require '${CLOAK_ROOT}/lib/Cloak/DomainRepository.php';
try {
\$pdo = DomainRepository::pdo();
DomainRepository::ensureTable(\$pdo);
\$host = DomainRepository::normalizeHostname('${SITE_DOMAIN}');
if (!DomainRepository::findByHostname(\$pdo, \$host)) {
DomainRepository::add(\$pdo, \$host);
}
} catch (Throwable \$e) {}
" 2>/dev/null || true
fi
nginx -t 2>/dev/null && nginx -s reload 2>/dev/null || true
+5
View File
@@ -0,0 +1,5 @@
#!/bin/bash
# 宝塔删站钩子(可选):不删除 check_config,仅清理 vhost 标记由面板完成
set -euo pipefail
exit 0
+131
View File
@@ -0,0 +1,131 @@
#!/bin/bash
# IPCLOAKAPI 宝塔一键安装脚本
# 用法:sudo bash install_bt.sh [--upgrade] [--source /path/to/cloaka]
set -euo pipefail
UPGRADE=0
SOURCE_DIR=""
CLOAK_ROOT="/www/cloaka"
ADMIN_DOMAIN=""
PHP_VER="82"
while [[ $# -gt 0 ]]; do
case "$1" in
--upgrade) UPGRADE=1; shift ;;
--source) SOURCE_DIR="${2:-}"; shift 2 ;;
--root) CLOAK_ROOT="${2:-}"; shift 2 ;;
*) echo "未知参数: $1"; exit 1 ;;
esac
done
if [[ "$(id -u)" -ne 0 ]]; then
echo "请使用 root 运行:sudo bash install_bt.sh"
exit 1
fi
if [[ ! -d /www/server/panel ]]; then
echo "未检测到宝塔面板目录 /www/server/panel"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ -z "$SOURCE_DIR" ]]; then
SOURCE_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
fi
detect_php() {
for v in 82 83 81 80 74; do
if [[ -f "/www/server/php/${v}/etc/php.ini" ]]; then
PHP_VER="$v"
return
fi
done
PHP_VER="82"
}
detect_php
PHP_INCLUDE="/www/server/nginx/conf/enable-php-${PHP_VER}.conf"
if [[ ! -f "$PHP_INCLUDE" ]]; then
PHP_INCLUDE="enable-php.conf"
fi
echo "=== IPCLOAKAPI 宝塔安装 ==="
echo "源码目录: $SOURCE_DIR"
echo "安装目录: $CLOAK_ROOT"
echo "PHP 版本: ${PHP_VER}"
if [[ "$UPGRADE" -eq 0 ]]; then
read -r -p "管理后台域名(如 cloak-admin.example.com,可留空稍后手动建站): " ADMIN_DOMAIN
fi
mkdir -p "$CLOAK_ROOT"
if command -v rsync >/dev/null 2>&1; then
rsync -a --delete \
--exclude '.git' --exclude 'node_modules' --exclude '.cursor' \
--exclude 'cong.php' --exclude '.install.lock' --exclude 'check_config/*_config.php' \
"$SOURCE_DIR/" "$CLOAK_ROOT/"
else
cp -a "$SOURCE_DIR/." "$CLOAK_ROOT/"
fi
chown -R www:www "$CLOAK_ROOT"
chmod -R u=rwX,g=rX,o=rX "$CLOAK_ROOT"
chmod -R u=rwX,g=rwX,o= "$CLOAK_ROOT/check_config" 2>/dev/null || mkdir -p "$CLOAK_ROOT/check_config" && chown www:www "$CLOAK_ROOT/check_config"
touch "$CLOAK_ROOT/jump.txt" "$CLOAK_ROOT/nt.txt" "$CLOAK_ROOT/err.txt" 2>/dev/null || true
chown www:www "$CLOAK_ROOT"/*.txt 2>/dev/null || true
# 注册建站钩子
HOOK_DIR="/www/server/panel/hook"
mkdir -p "$HOOK_DIR"
install -m 755 "$SCRIPT_DIR/hook/post_site.sh" "$HOOK_DIR/cloaka_post_site.sh"
install -m 755 "$SCRIPT_DIR/hook/post_site_del.sh" "$HOOK_DIR/cloaka_post_site_del.sh"
# 环境变量供钩子使用
ENV_FILE="/etc/cloaka.env"
echo "CLOAK_ROOT=${CLOAK_ROOT}" > "$ENV_FILE"
chmod 644 "$ENV_FILE"
# 预处理 Nginx 片段
SNIPPET_INST="${CLOAK_ROOT}/deploy/baota/nginx/cloaka-site.conf.installed"
sed -e "s|__CLOAK_ROOT__|${CLOAK_ROOT}|g" -e "s|__PHP_INCLUDE__|${PHP_INCLUDE}|g" \
"$SCRIPT_DIR/nginx/cloaka-site.conf" > "$SNIPPET_INST"
if [[ -n "$ADMIN_DOMAIN" && "$UPGRADE" -eq 0 ]]; then
if command -v bt >/dev/null 2>&1; then
echo "正在创建管理站点: $ADMIN_DOMAIN"
bt site add "$ADMIN_DOMAIN" "$CLOAK_ROOT" 2>/dev/null || true
fi
ADMIN_VHOST="/www/server/panel/vhost/nginx/${ADMIN_DOMAIN}.conf"
if [[ -f "$ADMIN_VHOST" ]] && ! grep -q 'cloaka-admin' "$ADMIN_VHOST" 2>/dev/null; then
cat "$SCRIPT_DIR/nginx/cloaka-admin.conf" >> "$ADMIN_VHOST"
fi
fi
# 确保域名表(已安装 cong 时)
if [[ -f "$CLOAK_ROOT/cong.php" ]]; then
php -r "
require '${CLOAK_ROOT}/cong.php';
require '${CLOAK_ROOT}/lib/Cloak/DomainRepository.php';
\$pdo = DomainRepository::pdo();
DomainRepository::ensureTable(\$pdo);
echo 'cloak_site_domains OK\n';
" 2>/dev/null || true
fi
nginx -t && nginx -s reload || true
echo ""
echo "============================================"
echo "安装文件已就绪。"
if [[ ! -f "$CLOAK_ROOT/.install.lock" ]]; then
if [[ -n "$ADMIN_DOMAIN" ]]; then
echo "请浏览器访问: https://${ADMIN_DOMAIN}/install.php"
else
echo "请在宝塔新建站点,根目录指向: ${CLOAK_ROOT}"
echo "然后访问: https://你的管理域名/install.php"
fi
else
echo "已检测到安装锁,进入后台: ${ADMIN_DOMAIN:-你的管理域名}/dashboard.php"
fi
echo "新建业务站点后,请在后台「站点域名」添加域名并关联配置。"
echo "============================================"
+10
View File
@@ -0,0 +1,10 @@
# IPCLOAKAPI — 管理站点:不经过 ip_check 流量判定,直接走 PHP
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include enable-php.conf;
}
+14
View File
@@ -0,0 +1,14 @@
# IPCLOAKAPI — 业务站点(占位符由 install_bt.sh / post_site.sh 替换)
# __CLOAK_ROOT__ __PHP_INCLUDE__
location @cloaka_entry {
include __PHP_INCLUDE__;
fastcgi_param SCRIPT_FILENAME __CLOAK_ROOT__/ip_check.php;
fastcgi_param SCRIPT_NAME /ip_check.php;
fastcgi_param REQUEST_URI $request_uri;
fastcgi_param QUERY_STRING $query_string;
}
location / {
try_files $uri $uri/ @cloaka_entry;
}
+5
View File
@@ -0,0 +1,5 @@
<?php
/**
* 可选:未使用 Nginx 集中路由时,将本站根目录 index.php 替换为本文件
*/
require '/www/cloaka/ip_check.php';
+7
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored Executable
+217
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+8
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+199
View File
@@ -0,0 +1,199 @@
<?php
date_default_timezone_set('Asia/Shanghai');
$projectRoot = dirname(dirname(__FILE__));
require_once $projectRoot . '/cong.php';
require_once $projectRoot . '/config/ConfigLoader.php';
require_once $projectRoot . '/lib/CloakHttpHelpers.php';
$config_name = isset($_GET['c']) ? strip_tags($_GET['c']) : 'index';
ConfigLoader::loadByName($config_name, $projectRoot);
if (!defined('CHECK_KEY')) {
header('HTTP/1.1 500 Internal Server Error');
header('Content-Type: text/plain; charset=utf-8');
echo '// fingerprint script: CHECK_KEY is not configured';
exit;
}
header('Content-Type: application/javascript; charset=utf-8');
$lang = strtoupper(CLOAK_ZH_ON) == 'ON' ? true : false;
$client = strtoupper(CLOAK_MOBILE_ON) == 'ON' ? true : false;
$os = strtoupper(CLOAK_OS_ON) == 'ON' ? true : false;
$is_virtual = intval(IS_VIRTUAL) == 0 ? false : IS_VIRTUAL;
$imodel = intval(IPHONE_MODEL) == 0 ? false : IPHONE_MODEL;
$cloak_key = CHECK_KEY;
$campagin_id = COSTM_IP_SCORE;
$time = time();
$ckey = sha1($campagin_id . sha1($time . $cloak_key));
$fp_cookie_domain = cloak_fingerprint_cookie_domain();
readfile(__DIR__ . '/cloakjs');
echo "\n";
?>
var cresult = true;
var vbrowser = 'pc';
var br = new MobileDetect(window.navigator.userAgent);
var jsSrc = navigator.languages;
var iphone_model = MobileDevice.getModels();
var mreason = "";
if(br.mobile() == null) {
vbrowser = 'pc';
} else {
vbrowser = 'mobile';
}
<?php
if($lang == true) {
?>
for (let lan of jsSrc) {
if(lan.toLowerCase().indexOf('zh')!=-1)
{
cresult = false;
mreason = 1;
}
}
<?php
}
?>
<?php
if($client == true) {
?>
if(cresult)
{
if(br.mobile() == null) {
cresult = false;
mreason = 2;
}
}
<?php
}
?>
<?php
if($os == true) {
?>
if(cresult)
{
if(br.os() == 'AndroidOS' || br.os() == 'iOS' || br.os() == 'iPadOS') {
} else {
cresult = false;
mreason = 3;
}
}
<?php
}
?>
<?php
if ($imodel) {
?>
var imodel = <?php echo $imodel; ?>;
if(cresult && br.is('iPhone'))
{
var getPixels = [];
if(imodel == 8){
getPixels = ["375x812", "390x844", "393x852", "402x874", "414x896@3", "414x896@2", "428x926", "430x932", "440x956"];
}else if(imodel == 10){
getPixels = ["375x812", "390x844", "393x852", "402x874", "414x896@3", "414x896@2", "428x926", "430x932", "440x956"];
}else if (imodel == 11){
getPixels = ["390x844", "393x852", "402x874", "428x926", "430x932", "440x956"];
}else if (imodel == 12){
getPixels = ["390x844", "393x852", "402x874", "428x926", "430x932", "440x956"];
}
const dpr = window.devicePixelRatio
const screenResolution = `${window.screen.width}x${window.screen.height}`;
if(screenResolution == '414x896') {
var ppixel = screenResolution + '@' + dpr;
} else {
var ppixel = screenResolution;
}
if(iphone_model != "") {
if (getPixels.includes(ppixel)) {
} else {
cresult = false
mreason = 4;
}
} else {
cresult = false
mreason = 11;
}
}
<?php
}
?>
<?php if ($is_virtual): ?>
<?php require __DIR__ . '/../resources/virtual_device_detect.inc.php'; ?>
var virtual = <?php echo (int) $is_virtual; ?>;
<?php endif; ?>
function cloakFinishFingerprint() {
var fpCookieOpts = { path: '/', expires: 1, sameSite: 'Lax' };
<?php if ($fp_cookie_domain !== ''): ?>
fpCookieOpts.domain = <?php echo json_encode($fp_cookie_domain, JSON_UNESCAPED_UNICODE); ?>;
<?php endif; ?>
var fpCookies = (typeof Cookies !== 'undefined' && Cookies.withAttributes)
? Cookies.withAttributes(fpCookieOpts)
: Cookies;
fpCookies.set('ctime', '<?php echo $time; ?>');
fpCookies.set('cyyek', '<?php echo $ckey; ?>');
fpCookies.set('mmr', String(mreason));
fpCookies.set('vbrowser', vbrowser);
fpCookies.set('imodel', iphone_model || '');
if(cresult) {
fpCookies.set('cl', 'oklot');
} else {
fpCookies.set('cl', 'uklot');
}
if (typeof window.__cloakFingerprintDone === 'function') {
window.__cloakFingerprintDone();
}
}
<?php if ($is_virtual): ?>
if (cresult && virtual > 0 && window.CloakVirtualDetect) {
CloakVirtualDetect.run(virtual, {
isMobile: br.mobile() != null,
ua: navigator.userAgent,
platform: navigator.platform || ''
}).then(function(res) {
if (res && res.blocked) {
cresult = false;
mreason = res.mmr || 12;
}
cloakFinishFingerprint();
}).catch(function() {
cloakFinishFingerprint();
});
} else {
cloakFinishFingerprint();
}
<?php else: ?>
cloakFinishFingerprint();
<?php endif; ?>
Vendored Executable
+4
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+6
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+2
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+8
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+3
View File
File diff suppressed because one or more lines are too long
Vendored Executable
+1
View File
File diff suppressed because one or more lines are too long
Executable
View File
Executable
+157
View File
@@ -0,0 +1,157 @@
<?php
/**==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==*==**/
/**
* 客户端指纹信息风险评估处理器
* 接收前端发送的指纹数据并进行综合风险判断
*/
session_start();
// 设置响应头为JSON格式
header('Content-Type: application/json; charset=utf-8');
// 确保请求方法为POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode([
'status' => 'error',
'message' => '只允许POST请求'
]);
exit;
}
// 获取并解析请求数据
$hdata = $_POST['hdata'] ?? '';
$decodedString = base64_decode($hdata, true);
$fingerprint_info = is_string($decodedString) ? json_decode($decodedString, true) : null;
if (!is_array($fingerprint_info) || empty($fingerprint_info['domain'])) {
http_response_code(400);
echo json_encode([
'status' => 'error',
'message' => '无效的指纹数据',
]);
exit;
}
require_once __DIR__ . '/cong.php';
require_once __DIR__ . '/lib/DbHelper.php';
require_once __DIR__ . '/lib/bootstrap.php';
require_once __DIR__ . '/config/ConfigLoader.php';
require_once __DIR__ . '/lib/Cloak/FpUrlHelper.php';
$fp_host = parse_url($fingerprint_info['domain'], PHP_URL_HOST);
if (empty($fp_host)) {
$fp_host = $_SERVER['HTTP_HOST'] ?? '';
}
$config_name = ConfigLoader::loadByHost($fp_host);
$jsonData = [
'id' => COSTM_IP_SCORE,
'hdata' => $hdata,
'referer' => $fingerprint_info['referer'] ?? '',
'domain' => $fingerprint_info['domain'],
'ip' => $fingerprint_info['ip'] ?? '',
'risk_enhanced' => defined('CLOAK_RISK_ENHANCED') && strtoupper(CLOAK_RISK_ENHANCED) === 'ON' ? 1 : 0,
'risk_number' => CLOAK_RISK_NUMBER,
];
$ch = curl_init('https://www.tiktokba.com/cloak/byApiRisk');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4);
curl_setopt($ch, CURLOPT_USERAGENT, get_SERVER_value('HTTP_USER_AGENT'));
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_ENCODING, '');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-type: application/x-www-form-urlencoded',
'escloak-key: ' . CHECK_KEY,
]);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($jsonData));
curl_setopt($ch, CURLOPT_HEADERFUNCTION, 'forward_response_cookies');
if ($_COOKIE) {
curl_setopt($ch, CURLOPT_COOKIE, encode_visitor_cookies());
}
$return = curl_exec($ch);
if ($return === false) {
http_response_code(502);
echo json_encode([
'status' => 'error',
'message' => 'Curl error: ' . curl_error($ch),
]);
curl_close($ch);
exit;
}
curl_close($ch);
$return = json_decode($return, true);
if (!is_array($return)) {
http_response_code(502);
echo json_encode([
'status' => 'error',
'message' => '风控 API 返回无效',
]);
exit;
}
$_SESSION['visit_to_3'] = 3;
$response = [];
$response['reason'] = $return['reason'];
$response['result'] = $return['result'];
if($return['result']) {
$_SESSION["check_result"] = "true"; // 通过指纹参数
$resolved = FpUrlHelper::resolve($config_name, true);
$fp_url = $resolved['url'];
$response['link'] = $fp_url;
} else {
$_SESSION["check_result"] = "false"; // 未通过指纹参数
if(CLOAK_REDIRECT_METHOD == 'curl') {
$response['link'] = '';
} else {
$zp_url = DB_ZP;
$response['link'] = $zp_url;
}
}
$log_id = (int) ($fingerprint_info['log_id'] ?? 0);
$update_log = [];
$update_log['result'] = $response['result'] ? "true" : "false";
$update_log['reason'] = $response['reason'] ?? '';
$update_log['fp_url'] = trim((string) ($response['link'] ?? ''));
if ($update_log['fp_url'] === '') {
$update_log['fp_url'] = $update_log['result'] === 'true'
? FpUrlHelper::fallbackUrl()
: (defined('DB_ZP') ? (string) DB_ZP : '');
}
$servername = "localhost";
$username = DB_USERNAME;
$password = DB_PASSWORD;
$dbname = DB_NAME;
// Create connection
$conn = new mysqli($servername, $username, $password, $dbname);
// Check connection
if ($conn->connect_error) {
$error = "Connection failed: " . $conn->connect_error;
$handle = fopen(dirname(__FILE__) . "/err.txt", "a+");
fwrite($handle, date("Y-m-d H:i:s") . ' | ' . $error . "\n");
fclose($handle);
} elseif ($log_id > 0) {
$resultEsc = cloak_db_escape($conn, $update_log['result'] ?? '');
$reasonEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['reason'] ?? '')));
$fpUrlEsc = cloak_db_escape($conn, strip_tags(cloak_db_string($update_log['fp_url'] ?? '')));
$sql = "UPDATE `visitor_logs` SET `result`='{$resultEsc}',`reason`='{$reasonEsc}',`fp_url`='{$fpUrlEsc}' WHERE `id`='" . (int) $log_id . "'";
if ($conn->query($sql) != TRUE) {
$handle = fopen(dirname(__FILE__) . "/err.txt", "a+");
fwrite($handle, date("Y-m-d H:i:s") . ' | ' . "Error: " . $sql . "<br>" . $conn->error . "\n");
fclose($handle);
}
$conn->close();
}
// 记录日志
exit(json_encode($response));
Executable
+4
View File
@@ -0,0 +1,4 @@
<?php
require('ip_check.php');
Executable
+429
View File
@@ -0,0 +1,429 @@
<?php
/**
* install.php — IPCLOAKAPI 一键安装程序
*
* 功能:
* 1. 首次部署时展示配置表单(MySQL / Redis / 登录密码 / API 秘钥)
* 2. 提交后校验 MySQL 连接,创建数据表,写入 cong.php,写入 .install.lock 锁文件
* 3. 锁文件存在时拒绝重新安装(防止覆盖生产配置)
*/
define('LOCK_FILE', __DIR__ . '/.install.lock');
define('CONG_FILE', __DIR__ . '/cong.php');
define('CONG_BACKUP', __DIR__ . '/cong.php.bak');
require_once __DIR__ . '/lib/CloakPhpCompat.php';
$phpCompatReport = CloakPhpCompat::buildReport();
$errors = [];
$success = false;
$isLocked = file_exists(LOCK_FILE);
$schemaMsg = '';
// ── 重置安装锁(仅允许凭旧密码强制重置,用于迁移场景)──────────────────
if (!empty($_POST['action']) && $_POST['action'] === 'unlock') {
if (file_exists(CONG_FILE)) {
@include_once(CONG_FILE);
}
$oldPwd = defined('LOGIN_PASSWORD') ? LOGIN_PASSWORD : '';
if (!empty($_POST['unlock_password']) && $_POST['unlock_password'] === $oldPwd) {
@unlink(LOCK_FILE);
$isLocked = false;
} else {
$errors[] = '旧密码不正确,无法重置安装锁。';
}
}
// ── 表单提交处理 ────────────────────────────────────────────────────────
if (!$isLocked && !empty($_POST['action']) && $_POST['action'] === 'install') {
if (!$phpCompatReport['ok']) {
$errors = array_merge($errors, $phpCompatReport['errors']);
}
// 读取表单值
$db_host = trim($_POST['db_host'] ?? '127.0.0.1') ?: '127.0.0.1';
$db_name = trim($_POST['db_name'] ?? '');
$db_username = trim($_POST['db_username'] ?? '');
$db_password = $_POST['db_password'] ?? '';
$redis_enabled= (isset($_POST['redis_enabled']) && $_POST['redis_enabled'] === 'ON') ? 'ON' : 'OFF';
$redis_host = trim($_POST['redis_host'] ?? '127.0.0.1') ?: '127.0.0.1';
$redis_port = max(1, min(65535, (int)($_POST['redis_port'] ?? 6379)));
$redis_timeout= trim($_POST['redis_timeout']?? '0.5') ?: '0.5';
$redis_pwd = $_POST['redis_password'] ?? '';
$login_pwd = $_POST['login_password'] ?? '';
$check_key = trim($_POST['check_key'] ?? '');
$server_ip = trim($_POST['server_ip'] ?? '');
$cf_token = trim($_POST['cloudflare_api_token'] ?? '');
$cf_account = trim($_POST['cloudflare_account_id'] ?? '');
// 基础校验
if (empty($db_name)) $errors[] = '数据库名不能为空。';
if (empty($db_username)) $errors[] = '数据库用户名不能为空。';
if (empty($login_pwd)) $errors[] = '登录密码不能为空。';
// MySQL 连接测试并创建数据表
if (empty($errors)) {
$conn = @new mysqli($db_host, $db_username, $db_password, $db_name);
if ($conn->connect_error) {
$errors[] = 'MySQL 连接失败:' . $conn->connect_error;
} else {
require_once __DIR__ . '/lib/InstallSchema.php';
$schemaResult = InstallSchema::install($conn);
if (!$schemaResult['ok']) {
$errors[] = '数据库建表失败:' . ($schemaResult['error'] ?? '未知错误');
} else {
$schemaMsg = '已创建/确认数据表:' . implode('、', $schemaResult['tables']);
}
$conn->close();
}
}
// Redis 连接测试(可跳过)
$redisOk = null;
$redisMsg = '';
if (empty($errors) && $redis_enabled === 'ON') {
if (extension_loaded('redis')) {
try {
$r = new Redis();
if (!@$r->connect($redis_host, $redis_port, 0.5)) {
$redisMsg = 'Redis 连接失败,已跳过(安装仍可继续)。';
$redisOk = false;
} else {
if ($redis_pwd !== '') $r->auth($redis_pwd);
$r->ping();
$redisOk = true;
}
} catch (Throwable $e) {
$redisMsg = 'Redis 连接异常:' . $e->getMessage() . '(已跳过)';
$redisOk = false;
}
} else {
$redisMsg = 'phpredis 扩展未安装,Redis 功能不可用(已跳过)。';
$redisOk = false;
}
}
// 写入 cong.php
if (empty($errors)) {
// 备份旧文件
if (file_exists(CONG_FILE)) {
@copy(CONG_FILE, CONG_BACKUP);
}
$cong_content = '<?php' . PHP_EOL
. PHP_EOL
. '// error_reporting(E_ALL);' . PHP_EOL
. '// ini_set(\'display_errors\', 1);' . PHP_EOL
. PHP_EOL
. 'define(\'LOGIN_PASSWORD\', ' . var_export($login_pwd, true) . ');' . PHP_EOL
. PHP_EOL
. 'define(\'DB_USERNAME\', ' . var_export($db_username, true) . ');' . PHP_EOL
. 'define(\'DB_NAME\', ' . var_export($db_name, true) . ');' . PHP_EOL
. 'define(\'DB_PASSWORD\', ' . var_export($db_password, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- API 鉴权秘钥(全局通用,对接 tiktokba.com cloak 服务)---' . PHP_EOL
. 'define(\'CHECK_KEY\', ' . var_export($check_key, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- Redis 通用配置(可选)---' . PHP_EOL
. 'define(\'REDIS_ENABLED\', ' . var_export($redis_enabled, true) . ');' . PHP_EOL
. 'define(\'REDIS_HOST\', ' . var_export($redis_host, true) . ');' . PHP_EOL
. 'define(\'REDIS_PORT\', ' . (int)$redis_port . ');' . PHP_EOL
. 'define(\'REDIS_TIMEOUT\', ' . (float)$redis_timeout . ');' . PHP_EOL
. 'define(\'REDIS_PASSWORD\', ' . var_export($redis_pwd, true) . ');' . PHP_EOL
. PHP_EOL
. '// --- 访客日志异步写入(ON=队列落库,OFF=同步 INSERT---' . PHP_EOL
. 'define(\'VISITOR_LOG_ASYNC\', \'ON\');' . PHP_EOL
. PHP_EOL
. '// --- Cloudflare(可选;留空则域名仅本地登记,不走 API)---' . PHP_EOL
. 'define(\'SERVER_IP\', ' . var_export($server_ip, true) . ');' . PHP_EOL
. 'define(\'CLOUDFLARE_API_TOKEN\', ' . var_export($cf_token, true) . ');' . PHP_EOL
. 'define(\'CLOUDFLARE_ACCOUNT_ID\', ' . var_export($cf_account, true) . ');' . PHP_EOL;
if (file_put_contents(CONG_FILE, $cong_content) === false) {
$errors[] = '写入 cong.php 失败,请检查目录写入权限。';
} else {
// 写入锁文件
file_put_contents(LOCK_FILE, date('Y-m-d H:i:s') . ' installed');
$success = true;
}
}
}
// ── 工具函数 ─────────────────────────────────────────────────────────────
function h($s) { return htmlspecialchars((string)$s, ENT_QUOTES, 'UTF-8'); }
function post($k, $d = '') { return h($_POST[$k] ?? $d); }
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>IPCLOAKAPI 安装程序</title>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f0f2f5;font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;min-height:100vh;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;padding:40px 16px}
.logo{text-align:center;margin-bottom:32px}
.logo h1{font-size:1.7rem;color:#16baaa;font-weight:700}
.logo p{color:#666;font-size:.9rem;margin-top:6px}
.card{background:#fff;border-radius:10px;box-shadow:0 4px 20px rgba(0,0,0,.1);width:100%;max-width:600px;overflow:hidden}
.card-header{background:linear-gradient(135deg,#16baaa,#0e8d81);color:#fff;padding:18px 24px}
.card-header h2{font-size:1.1rem;font-weight:600;margin:0}
.card-body{padding:24px}
.section-title{font-size:.78rem;font-weight:700;text-transform:uppercase;letter-spacing:.8px;color:#1565c0;margin:20px 0 10px;padding-bottom:6px;border-bottom:2px solid #e3f2fd}
.section-title:first-child{margin-top:0}
.form-group{margin-bottom:14px}
.form-group label{display:block;font-size:.85rem;color:#444;margin-bottom:4px;font-weight:500}
.form-group input[type=text],.form-group input[type=password],.form-group input[type=number]{width:100%;padding:8px 12px;border:1px solid #ced4da;border-radius:5px;font-size:.9rem;transition:border .2s}
.form-group input:focus{outline:none;border-color:#16baaa;box-shadow:0 0 0 3px rgba(22,186,170,.15)}
.form-group small{color:#888;font-size:.78rem;margin-top:3px;display:block}
.toggle-row{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.toggle-row label{font-size:.85rem;color:#444;font-weight:500;white-space:nowrap}
.toggle-switch{position:relative;width:46px;height:24px;flex-shrink:0}
.toggle-switch input{opacity:0;width:0;height:0}
.slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#ccc;border-radius:24px;transition:.3s}
.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background:#fff;border-radius:50%;transition:.3s}
input:checked+.slider{background:#16baaa}
input:checked+.slider:before{transform:translateX(22px)}
.btn{display:inline-block;padding:10px 28px;border-radius:6px;font-size:.95rem;font-weight:600;cursor:pointer;border:none;text-decoration:none;transition:opacity .2s}
.btn:hover{opacity:.88}
.btn-primary{background:#16baaa;color:#fff;width:100%;margin-top:8px}
.btn-secondary{background:#6c757d;color:#fff}
.btn-sm{padding:6px 14px;font-size:.82rem}
.alert{padding:12px 16px;border-radius:6px;margin-bottom:16px;font-size:.88rem}
.alert-danger{background:#f8d7da;color:#721c24;border:1px solid #f5c6cb}
.alert-success{background:#d4edda;color:#155724;border:1px solid #c3e6cb}
.alert-warning{background:#fff3cd;color:#856404;border:1px solid #ffeeba}
.alert-info{background:#d1ecf1;color:#0c5460;border:1px solid #bee5eb}
.divider{border:none;border-top:1px solid #e9ecef;margin:20px 0}
.locked-notice{text-align:center;padding:24px}
.locked-notice .icon{font-size:3rem;margin-bottom:12px}
.locked-notice h3{color:#444;margin-bottom:8px}
.locked-notice p{color:#666;font-size:.88rem;margin-bottom:16px}
details{border:1px solid #dee2e6;border-radius:6px;margin-top:16px}
details summary{padding:10px 14px;cursor:pointer;font-size:.85rem;color:#666;user-select:none}
details summary:hover{background:#f8f9fa}
details .details-body{padding:16px}
.step-indicator{display:flex;gap:0;margin-bottom:28px}
.step{flex:1;text-align:center;padding:8px 4px;font-size:.78rem;color:#aaa;border-bottom:2px solid #dee2e6;font-weight:500}
.step.active{color:#16baaa;border-bottom-color:#16baaa;font-weight:600}
.step.done{color:#28a745;border-bottom-color:#28a745}
</style>
</head>
<body>
<div class="logo">
<h1>IPCLOAKAPI</h1>
<p>一键安装程序 · 首次部署配置向导</p>
</div>
<div class="card">
<div class="card-header">
<h2>环境初始化配置</h2>
</div>
<div class="card-body">
<?php
$phpOk = $phpCompatReport['ok'];
$phpAlertClass = $phpOk ? 'alert-info' : 'alert-danger';
?>
<div class="alert <?php echo $phpAlertClass; ?>">
<strong>PHP 运行环境:</strong>
<?php echo htmlspecialchars(PHP_VERSION); ?>
<?php if ($phpOk): ?>
— 满足 Cloaka 要求(>= <?php echo CloakPhpCompat::MIN_PHP_VERSION; ?>
<?php else: ?>
— <strong>不满足最低要求</strong>,请升级至 PHP <?php echo CloakPhpCompat::MIN_PHP_VERSION; ?>+
<ul style="margin:8px 0 0 18px;padding:0;">
<?php foreach ($phpCompatReport['errors'] as $pe): ?>
<li><?php echo htmlspecialchars($pe); ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
<?php if (!empty($phpCompatReport['warnings'])): ?>
<div style="margin-top:8px;font-size:.85rem;">
<?php foreach ($phpCompatReport['warnings'] as $pw): ?>
<div>⚠ <?php echo htmlspecialchars($pw); ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php if ($isLocked && empty($errors)): ?>
<!-- ── 已安装状态 ── -->
<div class="locked-notice">
<div class="icon">🔒</div>
<h3>系统已安装</h3>
<p>安装锁文件存在,安装程序已被禁用,以防止覆盖生产配置。<br>如需重新配置,请输入当前登录密码以解锁。</p>
<a href="dashboard.php" class="btn btn-primary" style="width:auto;display:inline-block;margin-bottom:16px;">进入管理后台</a>
</div>
<details>
<summary>⚙️ 强制重新配置(危险操作,需验证旧密码)</summary>
<div class="details-body">
<div class="alert alert-warning">此操作将覆盖现有 cong.php,当前配置会备份到 cong.php.bak。</div>
<form method="post">
<input type="hidden" name="action" value="unlock">
<div class="form-group">
<label>当前登录密码</label>
<input type="password" name="unlock_password" placeholder="输入当前后台密码以解锁">
</div>
<button type="submit" class="btn btn-secondary btn-sm">验证并解锁</button>
</form>
</div>
</details>
<?php elseif ($success): ?>
<!-- ── 安装成功 ── -->
<div class="alert alert-success">
<strong>安装成功!</strong> cong.php 已写入,安装锁已创建。
</div>
<?php if (!empty($schemaMsg)): ?>
<div class="alert alert-info"><?php echo h($schemaMsg); ?></div>
<?php endif; ?>
<?php if (!empty($redisMsg)): ?>
<div class="alert alert-warning"><?php echo h($redisMsg); ?></div>
<?php endif; ?>
<p style="color:#555;font-size:.9rem;margin-bottom:20px;">系统已完成初始化配置,请立即进入后台开始使用。</p>
<a href="dashboard.php" class="btn btn-primary">进入管理后台 →</a>
<?php else: ?>
<!-- ── 安装表单 ── -->
<?php if (!empty($errors)): ?>
<div class="alert alert-danger">
<?php foreach ($errors as $e): ?><div>✗ <?php echo h($e); ?></div><?php endforeach; ?>
</div>
<?php endif; ?>
<?php if (!empty($redisMsg) && empty($errors)): ?>
<div class="alert alert-warning"><?php echo h($redisMsg); ?></div>
<?php endif; ?>
<form method="post" id="installForm">
<input type="hidden" name="action" value="install">
<!-- MySQL 配置 -->
<div class="section-title">MySQL 数据库</div>
<div class="form-group">
<label>数据库主机</label>
<input type="text" name="db_host" value="<?php echo post('db_host','127.0.0.1'); ?>" placeholder="127.0.0.1">
<small>通常为 127.0.0.1 或 localhost</small>
</div>
<div class="form-group">
<label>数据库名 <span style="color:#dc3545">*</span></label>
<input type="text" name="db_name" value="<?php echo post('db_name'); ?>" placeholder="如:cloaka_db" required>
<small>安装时将自动创建 ip_groups、ip_list、visitor_logs 数据表</small>
</div>
<div class="form-group">
<label>数据库用户名 <span style="color:#dc3545">*</span></label>
<input type="text" name="db_username" value="<?php echo post('db_username'); ?>" placeholder="如:cloaka_user" required>
</div>
<div class="form-group">
<label>数据库密码</label>
<input type="password" name="db_password" value="" placeholder="数据库用户密码" autocomplete="new-password">
</div>
<hr class="divider">
<!-- Redis 配置 -->
<div class="section-title">Redis 缓存(可选)</div>
<div class="toggle-row">
<label>启用 Redis</label>
<label class="toggle-switch">
<input type="checkbox" id="redisToggle" name="redis_enabled" value="ON" <?php echo (($_POST['redis_enabled'] ?? 'ON') === 'ON') ? 'checked' : ''; ?>>
<span class="slider"></span>
</label>
<small style="color:#888">phpredis 扩展<?php echo extension_loaded('redis') ? ' <span style="color:#28a745">✓ 已安装</span>' : ' <span style="color:#dc3545">✗ 未安装</span>'; ?></small>
</div>
<div id="redisFields">
<div class="form-group">
<label>Redis 主机</label>
<input type="text" name="redis_host" value="<?php echo post('redis_host','127.0.0.1'); ?>" placeholder="127.0.0.1">
</div>
<div class="form-group">
<label>Redis 端口</label>
<input type="number" name="redis_port" value="<?php echo post('redis_port','6379'); ?>" min="1" max="65535" style="max-width:120px;">
</div>
<div class="form-group">
<label>连接超时(秒)</label>
<input type="text" name="redis_timeout" value="<?php echo post('redis_timeout','0.5'); ?>" style="max-width:100px;">
</div>
<div class="form-group">
<label>Redis 密码(无密码留空)</label>
<input type="password" name="redis_password" value="" placeholder="无密码留空" autocomplete="new-password">
</div>
</div>
<hr class="divider">
<!-- 系统安全 -->
<div class="section-title">系统安全</div>
<div class="form-group">
<label>后台登录密码 <span style="color:#dc3545">*</span></label>
<input type="password" name="login_password" value="" placeholder="请设置强密码" required autocomplete="new-password">
<small>用于登录 dashboard.php 后台管理页面</small>
</div>
<div class="form-group">
<label>API 秘钥(CHECK_KEY</label>
<input type="text" name="check_key" value="<?php echo post('check_key'); ?>" placeholder="ipcloakapi.com | cloak 服务的 API 秘钥">
<small>由服务商提供,可在安装后于 cong.php 中修改</small>
</div>
<hr class="divider">
<!-- Cloudflare(可选) -->
<div class="section-title">Cloudflare 域名自动化(可选)</div>
<div class="form-group">
<label>服务器公网 IPSERVER_IP</label>
<input type="text" name="server_ip" value="<?php echo post('server_ip'); ?>" placeholder="例如 203.0.113.10">
<small>NS 生效后用于创建 proxied 的 A 记录;可安装后在 cong.php 填写</small>
</div>
<div class="form-group">
<label>Cloudflare API Token</label>
<input type="password" name="cloudflare_api_token" value="" placeholder="需 Zone Edit、DNS Edit、SSL 权限" autocomplete="new-password">
<small>在 Cloudflare 控制台 → My Profile → API Tokens 创建</small>
</div>
<div class="form-group">
<label>Cloudflare Account ID</label>
<input type="text" name="cloudflare_account_id" value="<?php echo post('cloudflare_account_id'); ?>" placeholder="控制台右侧 Account ID">
<small>三项均留空时,域名功能保持本地登记(兼容宝塔手动建站)</small>
</div>
<hr class="divider">
<button type="submit" class="btn btn-primary" id="submitBtn">开始安装</button>
</form>
<?php endif; ?>
</div><!-- /card-body -->
</div><!-- /card -->
<p style="color:#aaa;font-size:.78rem;margin-top:20px;text-align:center;">
安装完成后请将 install.php 删除或设置服务器禁止访问,以保障安全。
</p>
<script>
(function() {
var toggle = document.getElementById('redisToggle');
var fields = document.getElementById('redisFields');
if (!toggle || !fields) return;
function updateFields() {
fields.style.display = toggle.checked ? '' : 'none';
}
toggle.addEventListener('change', updateFields);
updateFields();
var form = document.getElementById('installForm');
var btn = document.getElementById('submitBtn');
if (form && btn) {
form.addEventListener('submit', function() {
btn.disabled = true;
btn.textContent = '安装中...';
});
}
})();
</script>
</body>
</html>
+63
View File
@@ -0,0 +1,63 @@
-- Cloaka 安装程序自动执行的表结构(与 lib/InstallSchema.php 保持一致)
-- 使用 CREATE TABLE IF NOT EXISTS,重复安装不会覆盖已有数据
CREATE TABLE `cloak_site_domains` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`hostname` varchar(255) NOT NULL COMMENT '站点域名,不含协议与路径',
`config_name` varchar(64) NOT NULL DEFAULT '' COMMENT '关联 check_config 配置名',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`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 '上次检测时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_hostname` (`hostname`),
KEY `idx_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `ip_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '组名',
`type` enum('black','white') NOT NULL COMMENT '类型:black为黑名单,white为白名单',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `ip_list` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL COMMENT '所属组ID',
`ip_address` varchar(45) NOT NULL COMMENT 'IP地址',
PRIMARY KEY (`id`),
KEY `idx_group_ip` (`group_id`,`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE `visitor_logs` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
`visit_md5_code` char(32) NOT NULL DEFAULT 'loss' COMMENT '客户端标记',
`domain` varchar(35) NOT NULL COMMENT '域名',
`visit_date` datetime NOT NULL COMMENT '访问日期',
`IP` varchar(150) NOT NULL COMMENT 'IP',
`country` varchar(10) DEFAULT NULL COMMENT '国家',
`result` char(5) DEFAULT NULL COMMENT '判断结果',
`reason` varchar(100) DEFAULT NULL COMMENT '判断理由',
`referer` text DEFAULT NULL COMMENT '来源',
`vtimes` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '第几次访问',
`client` varchar(20) DEFAULT NULL COMMENT '客户端',
`browser` varchar(20) DEFAULT NULL COMMENT '浏览器',
`device` varchar(300) DEFAULT NULL COMMENT '设备',
`page` text DEFAULT NULL COMMENT '浏览页面',
`fp_url` text DEFAULT NULL COMMENT '跳转页面',
`language` varchar(300) DEFAULT NULL COMMENT '语言',
`user_agent` text DEFAULT NULL COMMENT 'User-Agent 完整值',
`http_referer` text DEFAULT NULL COMMENT 'HTTP Referer 完整值',
`accept_language_raw` text DEFAULT NULL COMMENT 'Accept-Language 完整值',
`judge_timing` text DEFAULT NULL COMMENT '判定用时 JSON',
`add_time` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '日志记录时间',
PRIMARY KEY (`id`),
KEY `IP` (`IP`),
KEY `idx_visit_date` (`visit_date`),
KEY `idx_result` (`result`),
KEY `idx_domain_date` (`domain`,`visit_date`),
KEY `idx_country` (`country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
Executable
+23911
View File
File diff suppressed because it is too large Load Diff
Executable
+157
View File
@@ -0,0 +1,157 @@
<?php
require_once("cong.php");
session_start();
// $begin_time = date("H:i:s");
// include("ip_check_config.php");
$my_url = $_SERVER['PHP_SELF'];
$my_file_name = substr($my_url, strrpos($my_url, '/') + 1);
$config_name = str_replace('.php', '', $my_file_name);
require_once __DIR__ . '/config/ConfigLoader.php';
$config_name = ConfigLoader::loadForRequest($config_name, $_SERVER['HTTP_HOST'] ?? '');
require_once __DIR__ . '/lib/bootstrap.php';
$__cloak_debug_on = defined('CLOAK_DEBUG_MODE') && strtoupper(CLOAK_DEBUG_MODE) === 'ON';
$__cloak_debug_started = microtime(true);
if ($__cloak_debug_on) {
CloakDebugPage::init();
unset($_SESSION['check_result']);
$_SESSION['visit_to_1'] = '1';
$_SESSION['visit_to_2'] = '1';
$_SESSION['visit_to_3'] = '1';
}
if (!function_exists('cloak_dbg_step')) {
/** DEBUG 流程步骤($line 请传 __LINE__,避免 require 入口导致 backtrace 指向 index.php */
function cloak_dbg_step($line, $title, $status, $detail = '', array $extra = [])
{
if (empty($GLOBALS['__cloak_debug_on'])) {
return;
}
CloakDebugPage::stepAt($line, $title, $status, $detail, $extra);
}
}
if (!function_exists('cloak_dbg_record')) {
/** DEBUG check_result 写入轨迹($line 请传 __LINE__ */
function cloak_dbg_record($line, $result, $reason, array $extra = [])
{
if (empty($GLOBALS['__cloak_debug_on'])) {
return;
}
CloakDebugPage::recordAt($line, $result, $reason, $extra);
}
}
//-----------------------------------------------ip check bof-----------------------------------------------
$v_info = new visitorInfo();
$v_info->get_visitor_Info(COSTM_IP_SCORE, CHECK_KEY, SHOW_SITE_COUNTRY);
$reason = '';
CheckPipeline::run();
if (!isset($_SESSION['check_result']) || $_SESSION['check_result'] === '') {
$_SESSION['check_result'] = 'false';
if ($reason === '') {
$reason = '未产生判定结果,默认安全页';
}
}
$reason = cloak_finalize_reason($reason, $_SESSION['check_result']);
//echo "<br />result:".$_SESSION["check_result"]."<br />";
//$end_time = date("H:i:s");
//echo "".$begin_time .' | '. $end_time."<br />";
if( WRITE_LOG == "1")
{
$c_ip = $_SESSION["gcu_ip"]?$_SESSION["gcu_ip"]:$_SERVER['REMOTE_ADDR'];
$fp=fopen(dirname(__FILE__)."/jump.txt","a+");
fwrite($fp,date("Y-m-d H:i:s").' | 来源判断 | ' . $reason . "|" . $_SESSION["check_result"].' | '.$config_name.' | '.$c_ip.' | '. get_SERVER_value('HTTP_REFERER') ."\n");
fclose($fp);
}
// get devices
require_once dirname(__FILE__) . '/Mobile-Detect/src/MobileDetect.php';
$detect = new \Detection\MobileDetect;
$deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer');
if($deviceType == 'tablet') {
$patterns = $detect::getTabletDevices();
} elseif($deviceType == 'phone') {
$patterns = $detect::getPhoneDevices();
} elseif($deviceType == 'computer') {
$patterns = "";
$model = "PC";
}
if($patterns) {
foreach ($patterns as $brand => $pattern) {
if (preg_match('/' . $pattern . '/i', $_SERVER['HTTP_USER_AGENT'], $matches)) {
$model = $brand . ' ' . (isset($matches[1])?$matches[1]:"");
break;
}
}
}
if (trim($model) == 'iPhone') {
$model = !empty($_COOKIE['imodel'])?strip_tags($_COOKIE['imodel']):"iPhone";
}
$logs = array();
$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'] = $_SESSION["check_result"];
$logs['device'] = $model;
$logs['reason'] = $reason;
if ($__cloak_debug_on) {
$__force_risk_dbg = (defined('CLOAK_RISK_NUMBER') && CLOAK_RISK_NUMBER > 0 && CLOAK_RISK_NUMBER <= 90);
CloakDebugPage::setExitPoint(__LINE__, '判定完成 → CloakDebugPage::renderAndExit()(正常截停)', [
'check_result' => isset($_SESSION['check_result']) ? $_SESSION['check_result'] : 'false',
'reason' => $reason,
]);
CloakDebugPage::renderAndExit([
'config_name' => $config_name,
'check_result' => isset($_SESSION['check_result']) ? $_SESSION['check_result'] : 'false',
'reason' => $reason,
'visitor' => $v_info,
'logs' => $logs,
'device' => isset($model) ? $model : '',
'force_risk' => $__force_risk_dbg,
'mode' => SHOW_SITE_MODE_SWITCH,
'fp_urls' => defined('DB_FP') ? DB_FP : [],
'zp_url' => defined('DB_ZP') ? DB_ZP : '',
'started_at' => $__cloak_debug_started,
'session' => [
'check_result' => $_SESSION['check_result'] ?? '(未设置)',
'visit_to_1' => $_SESSION['visit_to_1'] ?? '',
'visit_to_2' => $_SESSION['visit_to_2'] ?? '',
'visit_to_3' => $_SESSION['visit_to_3'] ?? '',
'gcuid' => $_SESSION['gcuid'] ?? '',
'gcu_ip' => $_SESSION['gcu_ip'] ?? '',
],
]);
}
RouteResolver::dispatch([
'v_info' => $v_info,
'logs' => $logs,
'config_name' => $config_name,
'reason' => $reason,
'model' => isset($model) ? $model : '',
]);
?>
Executable
+43
View File
@@ -0,0 +1,43 @@
<!DOCTYPE html><html lang="en"><head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta content="width=device-width, initial-scale=1" name="viewport">
<title></title>
<script>
handler();
function jump(clerkType, clerkNumber, greetText) {
var url;
if (clerkType === "whatsapp") {
url = "https://api.whatsapp.com/send?phone=" + clerkNumber;
if (greetText) {
url += "&text=" + greetText;
}
} else if (clerkType === "line") {
url = "https://line.me/R/ti/p/~" + clerkNumber;
} else if (clerkType === "telegram") {
url = "https://t.me/" + clerkNumber;
} else if (clerkType === "link") {
url = clerkNumber;
}
if(clerkType != null && clerkType !== ""){
window.location.href = url;
}
}
function handler() {
var greetText = "Hello"
// whatsapp line telegram link
var clerkType = "link"
var kefu = [
"https://line.me/ti/p/9whJ5IH2KO"
"https://chat.whatsapp.com/Dr8UllnanjC5R2dxPq8Dyd"
]
var index = Math.floor(Math.random() * kefu.length);
var clerkNumber = kefu[index];
jump(clerkType, clerkNumber, greetText)
}
</script>
</body></html>
Executable
+95
View File
@@ -0,0 +1,95 @@
2026-05-24 20:19:53 | 来源判断 | |false | index | 127.0.0.1 |
2026-05-24 20:19:53 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('','1031c4dd4b0c','cloaka.test','2026-05-24 20:19:53','127.0.0.1','', 'false','','off','PC','chrome','https://cloaka.test:443/index.php?clk_ts=1779624149&clk_nonce=fe833f043be432f1&clk_sig=f5f6690067758f79c085c18aab3a36cd781224e1abb3e9ee5436f9132538d373','English','https://worldhomenest.store','PC')
2026-05-24 20:19:59 | 来源判断 | 缓存|false | index | 127.0.0.1 | https://cloaka.test/dashboard
2026-05-24 20:19:59 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('','','cloaka.test','2026-05-24 20:19:59','127.0.0.1','US', 'false','缓存','https://cloaka.test/dashboard','PC','chrome','https://cloaka.test:443/index.php?','English','https://worldhomenest.store','PC')
2026-05-24 20:25:12 | 来源判断 | |false | index | 127.0.0.1 |
2026-05-24 20:25:12 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('','a2ea7ffe92a8','cloaka.test','2026-05-24 20:25:12','127.0.0.1','', 'false','','off','PC','chrome','https://cloaka.test:443/index.php?clk_ts=1779625346&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=e02ebad7ce4fa0a4aeda23cb0cae63dbb70479d352b72fc677293f51c9ae66af','English','https://worldhomenest.store','PC')
2026-05-24 21:07:53 | 来源判断 | |false | index | 127.0.0.1 |
2026-05-24 21:38:24 | 来源判断 | |false | index | 127.0.0.1 |
2026-05-24 21:51:28 | 来源判断 | API响应非JSON|false | index | 127.0.0.1 |
2026-05-24 21:56:56 | 来源判断 | API请求失败:|false | index | 127.0.0.1 |
2026-05-24 21:59:15 | 来源判断 | API请求失败:|false | index | 127.0.0.1 |
2026-05-24 22:02:15 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-24 22:04:28 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-24 22:04:28 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','4ab064600a83','cloaka.test','2026-05-24 22:04:28','127.0.0.1','-', 'false','黑名单','off','PC','chrome','https://cloaka.test:443/index.php?clk_ts=1779631009&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=3698790e84c74380c19b29b0d9bfd0240a933ba54dc0d16625405b37f096b8a1','English','https://worldhomenest.store','PC')
2026-05-24 22:16:41 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-24 22:31:42 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-24 22:35:41 | 来源判断 | 来源限制:未命中已开启广告来源(Facebook)|false | index | 127.0.0.1 |
2026-05-25 02:48:05 | 来源判断 | 来源限制:未命中已开启广告来源(Facebook)|false | index | 127.0.0.1 |
2026-05-25 02:57:31 | 来源判断 | 来源限制:未命中已开启广告来源(Facebook)|false | index | 127.0.0.1 |
2026-05-25 02:57:31 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','','cloaka.test','2026-05-25 02:57:31','127.0.0.1','US', 'false','来源限制:未命中已开启广告来源(Facebook)','','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','English','https://worldhomenest.store','PC')
2026-05-25 03:01:56 | 来源判断 | 来源限制:未命中已开启广告来源(Facebook)|false | index | 127.0.0.1 |
2026-05-25 03:01:56 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','','cloaka.test','2026-05-25 03:01:56','127.0.0.1','US','false','来源限制:未命中已开启广告来源(Facebook)','','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','English','https://worldhomenest.store','PC')
2026-05-25 03:05:20 | 来源判断 | 来源限制:未命中已开启广告来源(Facebook)|false | index | 127.0.0.1 |
2026-05-25 03:05:20 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','','cloaka.test','2026-05-25 03:05:20','127.0.0.1','US','false','来源限制:未命中已开启广告来源(Facebook)','','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','English','https://worldhomenest.store','PC')
2026-05-25 03:33:18 | 来源判断 | 指纹Cookie未写入(已完成前端检测)|false | index | 127.0.0.1 | https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187
2026-05-25 03:33:18 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','84f1efe602e2','cloaka.test','2026-05-25 03:33:18','127.0.0.1','US','false','指纹Cookie未写入(已完成前端检测)','https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187&time=1779651198719','English','https://worldhomenest.store','PC')
2026-05-25 03:45:30 | 来源判断 | 指纹Cookie未写入(已完成前端检测)|false | index | 127.0.0.1 | https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187
2026-05-25 03:45:30 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','e2f5fc622c3e','cloaka.test','2026-05-25 03:45:30','127.0.0.1','US','false','指纹Cookie未写入(已完成前端检测)','https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187&time=1779651930890','English','https://worldhomenest.store','PC')
2026-05-25 03:57:21 | 来源判断 | 可疑访客:操作系统非IOS或者安卓|false | index | 127.0.0.1 | https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187
2026-05-25 03:57:21 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','df32c6c95fb6','cloaka.test','2026-05-25 03:57:21','127.0.0.1','US','false','可疑访客:操作系统非IOS或者安卓','https://cloaka.test/index?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187&time=1779652640867','English','https://worldhomenest.store','PC')
2026-05-25 04:05:52 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-25 04:05:52 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','08c02abd8025','cloaka.test','2026-05-25 04:05:52','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?clk_ts=0&clk_nonce=3cfa5dc8e5cf98a7&clk_sig=954780561396c8b7df3623a21fe1c7a1f362c92e10a540cca4809c2413b9f187','Chinese','https://worldhomenest.store','PC')
2026-05-26 02:02:56 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 02:02:56','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 02:17:38 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:17:38 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','025bc6046566','cloaka.test','2026-05-26 02:17:38','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:18:16 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:18:16 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','945dddb376ef','cloaka.test','2026-05-26 02:18:16','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:18:35 | 来源判断 | 缓存|false | index | 127.0.0.1 |
2026-05-26 02:18:35 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','','cloaka.test','2026-05-26 02:18:35','127.0.0.1','-','false','缓存','','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:18:54 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:18:54 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','adc58b3764e8','cloaka.test','2026-05-26 02:18:54','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:34:28 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 02:34:28','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 02:36:56 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 02:36:56','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 02:39:07 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:39:07 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','fb5ef3770269','cloaka.test','2026-05-26 02:39:07','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:39:27 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:39:28 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','3adc7b9b2b81','cloaka.test','2026-05-26 02:39:28','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:43:17 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-05-26 02:43:17 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','6d593c89acf1','cloaka.test','2026-05-26 02:43:17','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/index.php?','English','https://worldhomenest.store','PC')
2026-05-26 02:44:28 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 02:44:28 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','98af2b462187','cloaka.test','2026-05-26 02:44:28','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 02:51:29 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 02:51:29','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 02:52:41 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 02:52:41 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','07cdbefc93dd','cloaka.test','2026-05-26 02:52:41','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 02:55:19 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 02:55:19','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 02:56:48 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 02:56:48 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','d9ee1f0cff7e','cloaka.test','2026-05-26 02:56:48','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 02:57:49 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 02:57:49 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','a332d9695ae9','cloaka.test','2026-05-26 02:57:49','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 02:58:05 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 02:58:05 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','9451a6af27f1','cloaka.test','2026-05-26 02:58:05','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 12:09:55 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 12:09:55','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 12:51:31 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 12:51:31','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 12:58:27 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 12:58:27 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','f0f613c783b5','cloaka.test','2026-05-26 12:58:27','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-05-26 13:01:16 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-05-26 13:01:16','127.0.0.1','','false','','','PC','','/','','','')
2026-05-26 13:12:07 | 来源判断 | API判定通过|true | index | 127.0.0.1 |
2026-05-26 13:12:07 | 写入数据库 | true | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','0c6370dab8fd','cloaka.test','2026-05-26 13:12:07','127.0.0.1','-','true','','off','PC','chrome','https://cloaka.test/index.php?','English','','PC')
2026-06-10 23:05:11 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-10 23:05:11','127.0.0.1','','false','','','PC','','/','','','')
2026-06-10 23:05:59 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-10 23:05:59','127.0.0.1','','false','','','PC','','/','','','')
2026-06-11 00:25:15 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-11 00:25:15','127.0.0.1','','false','','','PC','','/','','','')
2026-06-11 19:28:27 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-11 19:28:27','127.0.0.1','','false','','','PC','','/','','','')
2026-06-11 19:31:05 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-11 19:31:05','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 00:27:19 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 00:27:19','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 01:29:59 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 01:29:59','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 02:05:06 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 02:05:06','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 02:06:00 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 02:06:00','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 02:28:34 | 来源判断 | 缓存|false | index | 127.0.0.1 |
2026-06-14 02:28:34 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','','cloaka.test','2026-06-14 02:28:34','127.0.0.1','-','false','缓存','','PC','chrome','https://cloaka.test/','English','https://worldhomenest.store','PC')
2026-06-14 02:30:57 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-06-14 02:30:57 | 写入数据库 | false | index | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('20251108_emoUAVF6S','fa0c6342a353','cloaka.test','2026-06-14 02:30:57','127.0.0.1','-','false','黑名单','off','PC','chrome','https://cloaka.test/','English','https://worldhomenest.store','PC')
2026-06-14 03:03:47 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 03:03:47','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 03:04:01 | 写入数据库 | | regression_test | INSERT INTO `visitor_logs`(`campagin_id`, `visit_md5_code`, `domain`, `visit_date`, `IP`, `country`, `result`, `reason`, `referer`, `client`, `browser`, `page`, `language`, `fp_url`, `device`) VALUES ('reg_test','','reg.test','2026-06-14 03:04:01','127.0.0.1','','false','','','PC','','/','','','')
2026-06-14 03:07:20 | 写入数据库 | | regression_test | 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 ('reg_test','','reg.test','2026-06-14 03:07:20','127.0.0.1','','false','','','PC','','/','','','','','','','')
2026-06-14 03:23:32 | 来源判断 | 缓存|true | index | 127.0.0.1 |
2026-06-14 03:23:32 | 写入数据库 | true | index | 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 ('20251108_emoUAVF6S','','cloaka.test','2026-06-14 03:23:32','127.0.0.1','-','true','','','PC','chrome','https://cloaka.test/?utm_campaign={campaignid}&adgroup_id=__CID__&ad_spm_id=t0tTw4MJ6r','English','Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36','','en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7','{\"total_ms\":0.12,\"stages\":[{\"name\":\"Session快速路径\",\"ms\":0.04}]}','https://www.google.com/','PC')
2026-06-14 11:51:53 | 写入数据库 | | regression_test | 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 ('reg_test','','reg.test','2026-06-14 11:51:53','127.0.0.1','','false','','','PC','','/','','','','','','','')
2026-06-14 12:27:29 | 写入数据库 | | regression_test | async=1
2026-06-14 12:27:29 | 写入数据库 | | regression_test | async=1
2026-06-14 12:30:13 | 写入数据库 | | regression_test | async=1
2026-06-14 12:30:13 | 写入数据库 | | regression_test | async=1
2026-06-14 12:30:50 | 写入数据库 | | regression_test | async=1
2026-06-14 12:30:50 | 写入数据库 | | regression_test | async=1
2026-06-14 12:33:28 | 来源判断 | 黑名单|false | index | 127.0.0.1 |
2026-06-14 12:33:28 | 写入数据库 | false | index | async=1
+170
View File
@@ -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);
}
}
+76
View File
@@ -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();
}
}
+208
View File
@@ -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\/\\\.\=\?\+\-~`@\':!%#]|(&amp;)|&)+/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);
}
}
+257
View File
@@ -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,
];
}
}
+241
View File
@@ -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';
}
}
+114
View File
@@ -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 Cookief_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];
}
/**
* 真实页 / 安全页兜底 URLDB_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
View File
@@ -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;
}
}
+54
View File
@@ -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
}
}
}
+15
View File
@@ -0,0 +1,15 @@
<?php
/**
* 二次风控页(原 page_666.php,依赖调用方 $logs 等变量)
*/
class RiskPageRenderer
{
/**
* @param array $ctx 需含 v_info、log_idwrite_log_db 返回值);可选 logs
*/
public static function render(array $ctx = [])
{
extract($ctx, EXTR_SKIP);
require dirname(__DIR__, 3) . '/page_666.php';
}
}
+63
View File
@@ -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')
);
}
}
+137
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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']);
}
}
+101
View File
@@ -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;
}
}
+54
View File
@@ -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 ? '判定通过(真实页)' : '判定拒绝(安全页)';
}
}
+320
View File
@@ -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 活动 IDCOSTM_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";
}
}
+441
View File
@@ -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;
}
}
+218
View File
@@ -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);
}
}
}
+92
View File
@@ -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;
}
}
+226
View File
@@ -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;
}
}
+355
View File
@@ -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_idTEXT 字段异步 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();
}
}
+270
View File
@@ -0,0 +1,270 @@
<?php
/**
* 广告来源限制 — 4 级优先级检测 + 广告链接 query 拼装
*/
class CloakAdSourceGuard
{
const PLATFORM_FB = 'fb';
const PLATFORM_GOOGLE = 'google';
const PLATFORM_TIKTOK = 'tiktok';
private static $refererDomains = [
'fb.com',
'facebook.com',
'instagram.com',
'l.facebook.com',
'lm.facebook.com',
'm.facebook.com',
'google.com',
'googleadservices.com',
'doubleclick.net',
'youtube.com',
'googlesyndication.com',
'tiktok.com',
'tiktokv.com',
'bytedance.com',
'snssdk.com',
'pangleglobal.com',
'ads.tiktok.com',
];
private static $uaPatterns = [
'fban',
'fbav',
'instagram',
'googleads',
'adsbot-google',
'gsa/',
'tiktok',
'musical_ly',
'bytedancewebview',
'bytelocale',
'trill_',
];
private static $clidParams = ['fbclid', 'gclid', 'wbraid', 'gbraid', 'ttclid'];
private static $tiktokMacros = [
'__CAMPAIGN_ID__',
'__AID__',
'__CID__',
'__PLACEMENT__',
];
private static $fullTemplates = [
self::PLATFORM_FB => 'utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_content={{ad.name}}&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}}&site_source={{site_source_name}}&placement={{placement}}',
self::PLATFORM_GOOGLE => 'utm_source=google&utm_medium=cpc&utm_campaign={campaignid}&utm_content={adgroupid}&utm_term={keyword}&ad_id={creative}&network={network}&placement={placement}',
self::PLATFORM_TIKTOK => 'utm_source=tiktok&utm_medium=video_ad&utm_campaign=__CAMPAIGN_ID__&utm_content=__AID__&adgroup_id=__CID__&placement=__PLACEMENT__',
];
private static $multiPlatformParams = [
self::PLATFORM_FB => 'campaign_id={{campaign.id}}',
self::PLATFORM_GOOGLE => 'utm_campaign={campaignid}',
self::PLATFORM_TIKTOK => 'adgroup_id=__CID__',
];
/**
* @param array $config ad_fb, ad_google, ad_tiktok, ad_strict, ad_spm_id (bool/string)
* @param array $context referer, query, user_agent
* @return array { passed: bool, matched_by: string, reason: string }
*/
public static function evaluate(array $config, array $context)
{
$enabled = self::anyPlatformEnabled($config);
if (!$enabled) {
return ['passed' => true, 'matched_by' => 'guard_disabled', 'reason' => ''];
}
$query = $context['query'] ?? [];
$referer = strtolower((string) ($context['referer'] ?? ''));
$userAgent = strtolower((string) ($context['user_agent'] ?? ''));
$spmId = trim((string) ($config['ad_spm_id'] ?? ''));
// 优先级 1ad_spm_id 强验证
if ($spmId !== '' && isset($query['ad_spm_id']) && (string) $query['ad_spm_id'] === $spmId) {
return ['passed' => true, 'matched_by' => 'ad_spm_id', 'reason' => ''];
}
// 优先级 2clid 或 TikTok 动态宏
foreach (self::$clidParams as $param) {
if (!empty($query[$param])) {
return ['passed' => true, 'matched_by' => 'clid:' . $param, 'reason' => ''];
}
}
$queryString = self::buildQueryString($query);
foreach (self::$tiktokMacros as $macro) {
if (strpos($queryString, $macro) !== false) {
return ['passed' => true, 'matched_by' => 'tiktok_macro:' . $macro, 'reason' => ''];
}
}
// 优先级 3Referer
foreach (self::$refererDomains as $domain) {
if ($referer !== '' && strpos($referer, $domain) !== false) {
return ['passed' => true, 'matched_by' => 'referer:' . $domain, 'reason' => ''];
}
}
// 优先级 4User-Agent
foreach (self::$uaPatterns as $pattern) {
if ($userAgent !== '' && strpos($userAgent, $pattern) !== false) {
return ['passed' => true, 'matched_by' => 'ua:' . $pattern, 'reason' => ''];
}
}
$strict = !empty($config['ad_strict']);
if ($strict) {
return [
'passed' => false,
'matched_by' => 'none',
'reason' => '来源限制:未命中广告来源验证',
];
}
return [
'passed' => false,
'matched_by' => 'none',
'reason' => '未确定流量来源',
];
}
/**
* @param array $enabledPlatforms PLATFORM_* 常量列表
* @param string $spmId
* @return string query 字符串(不含前导 ?
*/
public static function buildAdLinkQuery(array $enabledPlatforms, $spmId)
{
$spmId = trim((string) $spmId);
if (empty($enabledPlatforms)) {
return $spmId !== '' ? 'ad_spm_id=' . rawurlencode($spmId) : '';
}
$parts = [];
if (count($enabledPlatforms) === 1) {
$platform = $enabledPlatforms[0];
if (isset(self::$fullTemplates[$platform])) {
$parts[] = self::$fullTemplates[$platform];
}
} else {
foreach ($enabledPlatforms as $platform) {
if (isset(self::$multiPlatformParams[$platform])) {
$parts[] = self::$multiPlatformParams[$platform];
}
}
}
if ($spmId !== '') {
$parts[] = 'ad_spm_id=' . rawurlencode($spmId);
}
return implode('&', $parts);
}
/**
* 生成 1012 位随机字母数字 ad_spm_id
*/
public static function generateSpmId()
{
$len = random_int(10, 12);
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$max = strlen($chars) - 1;
$id = '';
for ($i = 0; $i < $len; $i++) {
$id .= $chars[random_int(0, $max)];
}
return $id;
}
/**
* @param string $spmId
*/
public static function isValidSpmId($spmId)
{
return (bool) preg_match('/^[A-Za-z0-9]{10,12}$/', (string) $spmId);
}
/**
* @param array $config
*/
public static function anyPlatformEnabled(array $config)
{
return !empty($config['ad_fb'])
|| !empty($config['ad_google'])
|| !empty($config['ad_tiktok']);
}
/**
* 从当前站点配置读取广告来源限制参数
*/
public static function configFromDefines()
{
return [
'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 : '',
];
}
/**
* 合并 Referer query 与 $_GET(与流水线 Guard 一致)
*/
public static function buildMergedRequestQuery(array $get = null)
{
if ($get === null) {
$get = $_GET;
}
$refRaw = isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '';
$refParts = parse_url($refRaw);
$refQ = [];
if (!empty($refParts['query'])) {
parse_str($refParts['query'], $refQ);
}
return array_merge($refQ, $get);
}
/**
* 构建 evaluate 用的请求上下文
*/
public static function buildRequestContext(array $get = null)
{
return [
'referer' => isset($_SERVER['HTTP_REFERER']) ? (string) $_SERVER['HTTP_REFERER'] : '',
'query' => self::buildMergedRequestQuery($get),
'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? (string) $_SERVER['HTTP_USER_AGENT'] : '',
];
}
/**
* 临时链接参数:是否满足开始/续期计时的来源条件
* 已开启广告来源限制 → 走 4 级优先级规则;未开启 → 兼容旧 clid/ad_spm_id 触发
*/
public static function shouldStartUrlArgsTimer(array $config, array $context)
{
if (!self::anyPlatformEnabled($config)) {
$query = $context['query'] ?? [];
foreach (array_merge(self::$clidParams, ['ad_spm_id']) as $param) {
if (!empty($query[$param])) {
return true;
}
}
return false;
}
$result = self::evaluate($config, $context);
return !empty($result['passed']);
}
/**
* @param array $query
*/
private static function buildQueryString(array $query)
{
if (empty($query)) {
return '';
}
return http_build_query($query);
}
}
+626
View File
@@ -0,0 +1,626 @@
<?php
/**
* CloakDebugPage — DEBUG 模式调试结果页
*/
class CloakDebugPage
{
/** 判定逻辑所在文件(入口 index.php require 本文件,backtrace 无法可靠取行号) */
const IP_CHECK_FILE = 'ip_check.php';
/** @var array 完整判断流程步骤 */
private static $flow = [];
/** @var array check_result 写入轨迹 */
private static $trail = [];
/** @var array|null */
private static $apiResponse = null;
/** @var array */
private static $flags = [];
/** @var array|null */
private static $exitPoint = null;
/** @var float */
private static $startedAt = 0;
public static function init()
{
self::$flow = [];
self::$trail = [];
self::$apiResponse = null;
self::$flags = [];
self::$exitPoint = null;
self::$startedAt = microtime(true);
}
/**
* 记录流程步骤(请从 ip_check.php 使用 cloak_dbg_step(__LINE__, ...) 调用)
*/
public static function step($title, $status, $detail = '', array $extra = [])
{
$loc = self::callerLoc();
self::stepAt($loc['line'], $title, $status, $detail, $extra);
}
/**
* 记录流程步骤,指定 ip_check.php 行号
*/
public static function stepAt($line, $title, $status, $detail = '', array $extra = [])
{
self::$flow[] = [
'title' => $title,
'status' => $status,
'detail' => $detail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => $extra,
'at' => microtime(true),
];
}
/**
* 记录 check_result 写入(请从 ip_check.php 使用 cloak_dbg_record(__LINE__, ...) 调用)
*/
public static function record($result, $reason, array $extra = [])
{
$loc = self::callerLoc();
self::recordAt($loc['line'], $result, $reason, $extra);
}
/**
* 记录 check_result 写入,指定 ip_check.php 行号
*/
public static function recordAt($line, $result, $reason, array $extra = [])
{
$stage = isset($extra['stage']) ? (string)$extra['stage'] : 'unknown';
$entry = [
'result' => $result,
'reason' => $reason,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'stage' => $stage,
'extra' => $extra,
'at' => microtime(true),
];
self::$trail[] = $entry;
$decideDetail = $reason !== '' ? $reason : '(理由为空)';
if ($result === 'true' || $result === 'false') {
$decideDetail .= ' → check_result=' . $result;
self::$flags['final'] = $entry;
} elseif ($result === null) {
$decideDetail = ($reason !== '' ? $reason : '(理由为空)') . '(未写入 check_result,流程继续)';
}
self::$flow[] = [
'title' => self::stageTitle($stage),
'status' => ($result === 'true' || $result === 'false') ? 'decide' : 'warn',
'detail' => $decideDetail,
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'extra' => array_merge($extra, ['check_result' => $result]),
'at' => microtime(true),
];
}
public static function setApiResponse(array $response)
{
self::$apiResponse = $response;
}
public static function setFlag($key, $value)
{
self::$flags[$key] = $value;
}
public static function setExitPoint($line, $label, array $extra = [])
{
self::$exitPoint = [
'file' => self::IP_CHECK_FILE,
'line' => (int)$line,
'label' => $label,
'extra' => $extra,
];
}
public static function renderAndExit(array $ctx)
{
$result = (string)($ctx['check_result'] ?? 'false');
$reason = (string)($ctx['reason'] ?? '');
$final = self::$flags['final'] ?? null;
$isRealPage = ($result === 'true');
$elapsed = isset($ctx['started_at'])
? round((microtime(true) - $ctx['started_at']) * 1000)
: round((microtime(true) - self::$startedAt) * 1000);
$routeInfo = self::buildRouteInfo($ctx, $result);
$diagnosis = self::buildReasonDiagnosis($reason, $result, $ctx);
$flowHtml = self::buildFlowHtml();
$trailHtml = self::buildTrailHtml();
$sessionHtml = self::buildSessionHtml($ctx['session'] ?? []);
$verdictLabel = $isRealPage ? '真实页 (DB_FP)' : '安全页 (DB_ZP)';
$verdictClass = $isRealPage ? 'verdict-pass' : 'verdict-block';
$verdictIcon = $isRealPage ? '✓' : '✕';
$finalLoc = $final ? self::formatLoc($final['file'], $final['line']) : '—';
$finalStage = $final ? htmlspecialchars(self::stageTitle($final['stage'])) : '—';
$finalReason = $final ? htmlspecialchars($final['reason'] !== '' ? $final['reason'] : '(空)') : '—';
$exit = self::$exitPoint;
$exitLoc = $exit ? self::formatLoc($exit['file'], $exit['line']) : self::IP_CHECK_FILE . ':?';
$exitLabel = $exit ? htmlspecialchars($exit['label']) : 'CloakDebugPage::renderAndExit()';
header('Content-Type: text/html; charset=utf-8');
header('X-Robots-Tag: noindex, nofollow');
echo '<!DOCTYPE html><html lang="zh-CN"><head><meta charset="utf-8">';
echo '<meta name="viewport" content="width=device-width,initial-scale=1">';
echo '<title>Cloak DEBUG — ' . htmlspecialchars($ctx['config_name'] ?? '') . '</title>';
echo self::styles();
echo '</head><body><div class="wrap">';
echo '<header class="hero">';
echo '<div class="hero-badge">DEBUG MODE</div>';
echo '<h1>流量判定调试报告</h1>';
echo '<p class="hero-sub">配置 <code>' . htmlspecialchars($ctx['config_name'] ?? '') . '</code>';
echo ' · 耗时 ' . (int)$elapsed . ' ms · ' . date('Y-m-d H:i:s') . '</p>';
echo '</header>';
// 流程终止位置(醒目)
echo '<section class="card exit-card">';
echo '<h2>流程终止位置</h2>';
echo '<div class="exit-main"><code>' . htmlspecialchars($exitLoc) . '</code></div>';
echo '<p class="exit-desc">DEBUG 在此截停执行(<strong>' . $exitLabel . '</strong>),未继续跳转真实页/安全页。</p>';
echo '<dl class="kv kv-tight">';
echo '<dt>未开启 DEBUG 时下一分支</dt><dd>' . htmlspecialchars($routeInfo['next_branch']) . '</dd>';
echo '<dt>预计跳转目标</dt><dd class="url-break">' . htmlspecialchars($routeInfo['dest'] !== '' ? $routeInfo['dest'] : '—') . '</dd>';
echo '<dt>路由条件</dt><dd><code>' . htmlspecialchars($routeInfo['condition']) . '</code></dd>';
echo '</dl></section>';
echo '<section class="card verdict-card ' . $verdictClass . '">';
echo '<div class="verdict-icon">' . $verdictIcon . '</div>';
echo '<div class="verdict-body">';
echo '<div class="verdict-title">' . htmlspecialchars($verdictLabel) . '</div>';
echo '<div class="verdict-meta">check_result = <code>' . htmlspecialchars($result) . '</code></div>';
echo '</div></section>';
// 判断理由 + 空理由诊断
echo '<section class="card"><h2>判定结论</h2><dl class="kv">';
echo '<dt>当前 $reason</dt><dd>';
if ($reason !== '') {
echo htmlspecialchars($reason);
} else {
echo '<span class="tag tag-warn">为空</span>';
}
echo '</dd>';
echo '<dt>最终写入 check_result</dt><dd><code>' . htmlspecialchars($finalLoc) . '</code>' . $finalStage . '</dd>';
echo '<dt>写入时 $reason</dt><dd>' . $finalReason . '</dd>';
echo '</dl>';
if ($diagnosis !== '') {
echo '<div class="diagnosis-box">' . $diagnosis . '</div>';
}
echo '</section>';
// 完整判断流程
echo '<section class="card"><h2>完整判断流程</h2>';
echo '<p class="hint">按 ip_check.php 实际执行顺序展示;<span class="legend-dot decide"></span> 表示写入 check_result<span class="legend-dot fail"></span> 拦截,<span class="legend-dot skip"></span> 跳过。</p>';
echo $flowHtml !== '' ? $flowHtml : '<p class="hint">暂无流程记录(请确认 CLOAK_DEBUG_MODE 已保存为 ON)。</p>';
echo '</section>';
if ($trailHtml !== '') {
echo '<section class="card"><h2>check_result 写入轨迹</h2>';
echo '<p class="hint">每次对 $_SESSION[\'check_result\'] 的赋值;最后一项为最终判定依据。</p>';
echo $trailHtml . '</section>';
}
if (self::$apiResponse) {
echo '<section class="card"><h2>API 判定接口 (cloak_check_curl)</h2>';
echo self::buildApiHtml(self::$apiResponse);
echo '</section>';
}
echo '<div class="grid-2">';
echo '<section class="card"><h2>Session 状态</h2>';
echo $sessionHtml . '</section>';
echo '<section class="card"><h2>路由预览</h2><dl class="kv kv-tight">';
echo '<dt>SHOW_SITE_MODE</dt><dd><code>' . htmlspecialchars($ctx['mode'] ?? '') . '</code></dd>';
echo '<dt>visit_to_2</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_2'] ?? '—') . '</code> <span class="hint-inline">=2 时跳过 API 块</span></dd>';
echo '<dt>visit_to_3</dt><dd><code>' . htmlspecialchars($ctx['session']['visit_to_3'] ?? '—') . '</code></dd>';
echo '</dl></section>';
echo '</div>';
echo '<section class="card"><h2>访客依据信息</h2>';
echo '<table class="data-table"><tbody>' . self::buildVisitorRows($ctx) . '</tbody></table></section>';
echo '<section class="card"><h2>请求参数</h2>';
echo '<table class="data-table"><tbody>' . self::buildQueryRows() . '</tbody></table></section>';
echo '<section class="card"><h2>配置快照</h2>';
echo '<table class="data-table compact"><tbody>' . self::buildConfigRows() . '</tbody></table></section>';
echo '<footer class="foot">DEBUG 模式 — 生产环境请关闭 CLOAK_DEBUG_MODE</footer>';
echo '</div></body></html>';
exit;
}
private static function callerLoc()
{
$bt = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 20);
foreach ($bt as $frame) {
if (empty($frame['file'])) {
continue;
}
if (basename($frame['file']) === self::IP_CHECK_FILE) {
return [
'file' => self::IP_CHECK_FILE,
'line' => (int)($frame['line'] ?? 0),
];
}
}
return [
'file' => self::IP_CHECK_FILE,
'line' => 0,
];
}
private static function formatLoc($file, $line)
{
$file = ($file !== '' && $file !== '—') ? $file : self::IP_CHECK_FILE;
if (strpos($file, 'ip_check') !== false) {
$file = self::IP_CHECK_FILE;
}
return $file . ':' . (int)$line;
}
private static function stageTitle($stage)
{
$map = [
'session_fast_path' => 'Session 快速路径',
'url_args_timeout' => '临时链接参数',
'blacklist' => '黑名单',
'whitelist_ip' => 'IP 白名单',
'whitelist_params' => '链接参数白名单',
'fingerprint_cookie' => 'JS 指纹 Cookie',
'api_cloak' => 'API 远程判定',
'no_referer_or_gcuid' => '无 Referer/gcuid',
'session_partial' => 'Session 已有结果',
'main_block_skipped' => '主判定块跳过',
];
return isset($map[$stage]) ? $map[$stage] : $stage;
}
private static function buildRouteInfo(array $ctx, $result)
{
$mode = $ctx['mode'] ?? '';
$isReal = ($mode !== 'zp') && ((($mode === 'ip_check' && $result !== 'false') || $mode === 'fp'));
$forceRisk = !empty($ctx['force_risk']);
$visit3 = $ctx['session']['visit_to_3'] ?? '';
if (!$isReal) {
return [
'next_branch' => '安全页分支 (ip_check.php ~859)',
'dest' => (string)($ctx['zp_url'] ?? ''),
'condition' => "mode={$mode}, check_result={$result} → false 或 mode=zp",
];
}
if ($forceRisk && $visit3 !== '3') {
return [
'next_branch' => '真实页 + 二次风控 (page_666.php / cloakjs)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => 'check_result!=false 且 CLOAK_RISK_NUMBER>0',
];
}
return [
'next_branch' => '真实页 (page_6.php)',
'dest' => self::firstUrl($ctx['fp_urls'] ?? []),
'condition' => "mode={$mode}, check_result={$result}",
];
}
private static function buildReasonDiagnosis($reason, $result, array $ctx)
{
if ($reason !== '') {
return '';
}
$items = [];
$final = self::$flags['final'] ?? null;
if (!$final) {
$items[] = '整个流程<strong>未记录</strong>到任何 <code>check_result</code> 写入点;最终 result 可能来自默认值或未赋值。';
} elseif ($final['reason'] === '') {
$items[] = '最终写入点 <code>' . htmlspecialchars(self::formatLoc($final['file'], $final['line'])) . '</code>' . htmlspecialchars(self::stageTitle($final['stage'])) . ')写入时 <code>$reason</code> 即为空。';
}
if (self::$apiResponse) {
$apiReason = self::$apiResponse['reason'] ?? '';
$apiCalled = !empty(self::$apiResponse['called']);
if ($apiCalled && $apiReason === '') {
$items[] = 'API 接口 <code>cloak_check_curl</code> 已调用,但返回 JSON 中 <code>reason</code> 字段为空(见下方 API 详情)。';
} elseif (!$apiCalled) {
$items[] = 'API 判定<strong>未执行</strong>(可能缺少 ip/Browser/Accept-Language,或主判定块被跳过)。';
}
}
$visit2 = $ctx['session']['visit_to_2'] ?? '';
if ($visit2 === '2' && empty(self::$apiResponse['called'])) {
$items[] = '<code>$_SESSION[\'visit_to_2\']=2</code> 导致主判定块(API 调用)被跳过;若同时 <code>check_result</code> 未重建,<code>$reason</code> 会保持初始空字符串。DEBUG 模式已重置 visit_to_*,若仍出现请检查是否在判定前被重新写入。';
}
if ($result === 'true') {
$items[] = '生产环境中,进入真实页分支后 <code>ip_check.php:818</code> 会将 <code>$logs[\'reason\']</code> 清空(注释:访问仿品时没用跳转理由),日志里可能显示空理由,但 DEBUG 截停前应仍能看到写入时的 reason。';
}
if (empty($items)) {
$items[] = '$reason 在流程开始时初始化为空字符串,且后续没有任何分支对其赋值。';
}
$html = '<h3 class="diag-title">⚠ $reason 为空 — 溯源分析</h3><ul class="diag-list">';
foreach ($items as $item) {
$html .= '<li>' . $item . '</li>';
}
$html .= '</ul>';
return $html;
}
private static function buildFlowHtml()
{
if (empty(self::$flow)) {
return '';
}
$html = '<div class="flow-timeline">';
$final = self::$flags['final'] ?? null;
foreach (self::$flow as $i => $s) {
$isFinal = $final
&& isset($s['extra']['check_result'])
&& ($s['extra']['check_result'] === 'true' || $s['extra']['check_result'] === 'false')
&& $final['line'] === $s['line']
&& $final['stage'] === ($s['extra']['stage'] ?? '');
$cls = 'flow-step status-' . htmlspecialchars($s['status']);
if ($isFinal) {
$cls .= ' flow-final';
}
$html .= '<div class="' . $cls . '">';
$html .= '<div class="flow-marker">' . ($i + 1) . '</div>';
$html .= '<div class="flow-body">';
$html .= '<div class="flow-head">';
$html .= '<span class="flow-title">' . htmlspecialchars($s['title']) . '</span>';
$html .= '<span class="flow-badge badge-' . htmlspecialchars($s['status']) . '">' . htmlspecialchars(self::statusLabel($s['status'])) . '</span>';
$html .= '<code class="flow-loc">' . htmlspecialchars(self::formatLoc($s['file'], $s['line'])) . '</code>';
$html .= '</div>';
if ($s['detail'] !== '') {
$html .= '<div class="flow-detail">' . htmlspecialchars($s['detail']) . '</div>';
}
if (!empty($s['extra'])) {
$show = $s['extra'];
unset($show['stage'], $show['check_result']);
if (!empty($show)) {
$html .= '<pre class="code-block sm">' . htmlspecialchars(json_encode($show, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre>';
}
}
$html .= '</div></div>';
}
$html .= '</div>';
return $html;
}
private static function statusLabel($status)
{
$map = [
'pass' => '通过', 'fail' => '拦截', 'skip' => '跳过',
'warn' => '可疑', 'info' => '信息', 'decide' => '写入结果',
];
return isset($map[$status]) ? $map[$status] : $status;
}
private static function buildTrailHtml()
{
if (empty(self::$trail)) {
return '';
}
$html = '<div class="trail">';
$final = self::$flags['final'] ?? null;
foreach (self::$trail as $e) {
$isFinal = $final && $final['line'] === $e['line'] && $final['stage'] === $e['stage'];
$cls = $isFinal ? ' trail-item final' : ' trail-item';
$res = $e['result'] !== null ? $e['result'] : '—';
$html .= '<div class="' . trim($cls) . '">';
$html .= '<div class="trail-head">';
$html .= '<span class="trail-loc"><code>' . htmlspecialchars(self::formatLoc($e['file'], $e['line'])) . '</code></span>';
$html .= '<span class="trail-stage">' . htmlspecialchars(self::stageTitle($e['stage'])) . '</span>';
if ($e['result'] !== null) {
$html .= '<span class="trail-res">' . htmlspecialchars((string)$res) . '</span>';
}
$html .= '</div>';
$html .= '<div class="trail-reason">' . htmlspecialchars($e['reason'] !== '' ? $e['reason'] : '(理由为空)') . '</div>';
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
private static function buildApiHtml(array $api)
{
$html = '<dl class="kv">';
$html .= '<dt>是否调用</dt><dd>' . (!empty($api['called']) ? '是' : '否') . '</dd>';
if (!empty($api['skip_reason'])) {
$html .= '<dt>未调用原因</dt><dd>' . htmlspecialchars($api['skip_reason']) . '</dd>';
}
$html .= '<dt>API result</dt><dd><code>' . htmlspecialchars((string)($api['result_raw'] ?? $api['check_result'] ?? '—')) . '</code></dd>';
$html .= '<dt>API reason</dt><dd>' . htmlspecialchars((string)($api['reason'] ?? '—')) . '</dd>';
$html .= '<dt>API country</dt><dd>' . htmlspecialchars((string)($api['country'] ?? '—')) . '</dd>';
if (!empty($api['curl_error'])) {
$html .= '<dt>Curl 错误</dt><dd class="tag tag-warn">' . htmlspecialchars($api['curl_error']) . '</dd>';
}
if (!empty($api['raw'])) {
$html .= '<dt>原始响应</dt><dd><pre class="code-block">' . htmlspecialchars(is_string($api['raw']) ? $api['raw'] : json_encode($api['raw'], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)) . '</pre></dd>';
}
$html .= '</dl>';
return $html;
}
private static function buildSessionHtml(array $session)
{
if (empty($session)) {
return '<p class="hint">无 Session 快照</p>';
}
$html = '<table class="data-table compact"><tbody>';
foreach ($session as $k => $v) {
$html .= '<tr><th>' . htmlspecialchars((string)$k) . '</th><td><code>' . htmlspecialchars(is_scalar($v) ? (string)$v : json_encode($v, JSON_UNESCAPED_UNICODE)) . '</code></td></tr>';
}
$html .= '</tbody></table>';
return $html;
}
private static function buildVisitorRows(array $ctx)
{
$v = $ctx['visitor'] ?? null;
$rows = [
'IP 地址' => $v ? $v->v_ip : ($ctx['logs']['customers_ip'] ?? ''),
'国家/地区' => $v ? $v->v_country : '',
'浏览器' => $v ? $v->v_Browser : ($ctx['logs']['v_Browser'] ?? ''),
'客户端类型' => $v ? $v->v_Client : ($ctx['logs']['Client'] ?? ''),
'Referer' => $v ? $v->v_referer : ($ctx['logs']['v_referer'] ?? ''),
'当前 URL' => $v ? $v->v_curPageURL : ($ctx['logs']['v_PageURL'] ?? ''),
'Accept-Language' => $v ? $v->accept_language : ($ctx['logs']['accept_language'] ?? ''),
'User-Agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '',
'visit_md5' => $v ? $v->visit_md5_code : ($ctx['logs']['visit_md5_code'] ?? ''),
'设备型号' => $ctx['device'] ?? '',
'gcuid' => $_SESSION['gcuid'] ?? '',
];
return self::rowsHtml($rows);
}
private static function buildQueryRows()
{
$keys = ['fbclid', 'gclid', 'wbraid', 'gbraid', 'ttclid', 'ad_spm_id'];
$rows = [];
foreach ($keys as $k) {
$rows[$k] = isset($_GET[$k]) ? $_GET[$k] : '(未携带)';
}
return self::rowsHtml($rows);
}
private static function buildConfigRows()
{
$map = [
'SHOW_SITE_MODE_SWITCH' => defined('SHOW_SITE_MODE_SWITCH') ? SHOW_SITE_MODE_SWITCH : '',
'SHOW_SITE_COUNTRY' => defined('SHOW_SITE_COUNTRY') ? SHOW_SITE_COUNTRY : '',
'CLOAK_DEBUG_MODE' => defined('CLOAK_DEBUG_MODE') ? CLOAK_DEBUG_MODE : '',
'CLOAK_RISK_NUMBER' => defined('CLOAK_RISK_NUMBER') ? CLOAK_RISK_NUMBER : '',
'CLOAK_RISK_ENHANCED' => defined('CLOAK_RISK_ENHANCED') ? CLOAK_RISK_ENHANCED : '',
'CLOAK_URL_ARGS_TIMEOUT' => defined('CLOAK_URL_ARGS_TIMEOUT') ? CLOAK_URL_ARGS_TIMEOUT : '',
];
return self::rowsHtml($map);
}
private static function rowsHtml(array $rows)
{
$html = '';
foreach ($rows as $label => $val) {
$html .= '<tr><th>' . htmlspecialchars((string)$label) . '</th><td>' . htmlspecialchars((string)$val) . '</td></tr>';
}
return $html;
}
private static function firstUrl($urls)
{
if (is_array($urls) && !empty($urls)) {
return (string)$urls[0];
}
return is_string($urls) ? $urls : '';
}
private static function styles()
{
return <<<'CSS'
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;background:linear-gradient(145deg,#0f172a,#1e293b 50%,#0f172a);color:#e2e8f0;min-height:100vh;line-height:1.55}
.wrap{max-width:980px;margin:0 auto;padding:24px 16px 48px}
.hero{text-align:center;margin-bottom:24px}
.hero-badge{display:inline-block;background:#f59e0b;color:#1e293b;font-size:.72rem;font-weight:700;letter-spacing:.08em;padding:4px 10px;border-radius:999px;margin-bottom:10px}
.hero h1{font-size:1.5rem;margin-bottom:6px}
.hero-sub{font-size:.85rem;color:#94a3b8}
.hero-sub code{background:#334155;padding:2px 6px;border-radius:4px}
.card{background:#1e293b;border:1px solid #334155;border-radius:12px;padding:20px;margin-bottom:16px}
.card h2{font-size:.92rem;font-weight:600;color:#94a3b8;text-transform:uppercase;letter-spacing:.04em;margin-bottom:14px}
.grid-2{display:grid;grid-template-columns:1fr 1fr;gap:16px}
@media(max-width:720px){.grid-2{grid-template-columns:1fr}}
.exit-card{border-color:#f59e0b;background:linear-gradient(135deg,#422006 0%,#1e293b 40%)}
.exit-main{font-size:1.4rem;font-weight:700;margin-bottom:8px}
.exit-main code{color:#fcd34d}
.exit-desc{font-size:.88rem;color:#cbd5e1;margin-bottom:12px}
.verdict-card{display:flex;align-items:center;gap:20px;padding:22px}
.verdict-pass{border-color:#059669;background:linear-gradient(135deg,#064e3b,#1e293b)}
.verdict-block{border-color:#dc2626;background:linear-gradient(135deg,#7f1d1d,#1e293b)}
.verdict-icon{width:52px;height:52px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;flex-shrink:0}
.verdict-pass .verdict-icon{background:#059669;color:#fff}
.verdict-block .verdict-icon{background:#dc2626;color:#fff}
.verdict-title{font-size:1.25rem;font-weight:700}
.verdict-meta{font-size:.85rem;color:#94a3b8;margin-top:4px}
.kv dt{font-size:.76rem;color:#64748b;margin-top:10px}
.kv dt:first-child{margin-top:0}
.kv dd{font-size:.9rem;margin-top:2px;word-break:break-all}
.kv-tight dt{margin-top:6px}
.diagnosis-box{margin-top:14px;padding:14px;background:#422006;border:1px solid #b45309;border-radius:8px;font-size:.85rem}
.diag-title{font-size:.88rem;color:#fcd34d;margin-bottom:8px}
.diag-list{padding-left:18px;color:#fde68a}
.diag-list li{margin-bottom:6px}
.flow-timeline{display:flex;flex-direction:column;gap:0;position:relative;padding-left:8px}
.flow-step{display:flex;gap:14px;padding:12px 0;border-bottom:1px solid #334155;position:relative}
.flow-step:last-child{border-bottom:none}
.flow-step.flow-final .flow-body{border-left:3px solid #f59e0b;padding-left:12px;margin-left:-12px}
.flow-marker{width:28px;height:28px;border-radius:50%;background:#334155;color:#94a3b8;font-size:.75rem;font-weight:700;display:flex;align-items:center;justify-content:center;flex-shrink:0;margin-top:2px}
.flow-step.status-decide .flow-marker{background:#b45309;color:#fff}
.flow-step.status-fail .flow-marker{background:#dc2626;color:#fff}
.flow-step.status-pass .flow-marker{background:#059669;color:#fff}
.flow-step.status-skip .flow-marker{background:#475569;color:#cbd5e1}
.flow-body{flex:1;min-width:0}
.flow-head{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin-bottom:4px}
.flow-title{font-weight:600;font-size:.92rem}
.flow-badge{font-size:.68rem;padding:2px 7px;border-radius:4px;font-weight:600}
.badge-pass{background:#064e3b;color:#6ee7b7}
.badge-fail{background:#7f1d1d;color:#fca5a5}
.badge-skip{background:#334155;color:#94a3b8}
.badge-warn{background:#78350f;color:#fcd34d}
.badge-info{background:#1e3a5f;color:#7dd3fc}
.badge-decide{background:#92400e;color:#fde68a}
.flow-loc{font-size:.72rem;color:#38bdf8}
.flow-detail{font-size:.85rem;color:#cbd5e1}
.legend-dot{display:inline-block;width:8px;height:8px;border-radius:50%;margin:0 2px}
.legend-dot.decide{background:#f59e0b}
.legend-dot.fail{background:#dc2626}
.legend-dot.skip{background:#64748b}
.hint{font-size:.8rem;color:#64748b;margin-bottom:12px}
.hint-inline{font-size:.72rem;color:#64748b}
.tag{display:inline-block;font-size:.72rem;padding:2px 8px;border-radius:4px}
.tag-warn{background:#78350f;color:#fcd34d}
.tag-info{background:#1e3a5f;color:#7dd3fc}
.url-break{word-break:break-all;color:#38bdf8;font-size:.85rem}
.data-table{width:100%;border-collapse:collapse;font-size:.84rem}
.data-table th{text-align:left;color:#64748b;font-weight:500;width:130px;padding:7px 10px 7px 0;vertical-align:top;border-bottom:1px solid #334155}
.data-table td{padding:7px 0;word-break:break-all;border-bottom:1px solid #334155}
.data-table.compact th{width:180px}
.data-table tr:last-child th,.data-table tr:last-child td{border-bottom:none}
.code-block{background:#0f172a;border:1px solid #334155;border-radius:6px;padding:10px;font-size:.76rem;overflow-x:auto;margin-top:6px;white-space:pre-wrap}
.code-block.sm{font-size:.7rem;padding:6px 8px}
.trail{display:flex;flex-direction:column;gap:8px}
.trail-item{background:#0f172a;border:1px solid #334155;border-radius:8px;padding:10px 12px}
.trail-item.final{border-color:#f59e0b}
.trail-head{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-bottom:4px}
.trail-loc code{font-size:.76rem;color:#38bdf8}
.trail-stage{font-size:.72rem;color:#64748b}
.trail-res{font-size:.72rem;background:#334155;padding:2px 7px;border-radius:4px;font-family:monospace}
.trail-reason{font-size:.85rem;color:#cbd5e1}
.foot{text-align:center;font-size:.76rem;color:#64748b;margin-top:20px}
</style>
CSS;
}
}
+255
View File
@@ -0,0 +1,255 @@
<?php
/**
* HTTP / Cookie 辅助函数(ip_check 与 f_check 共用)
*/
if (!function_exists('cloak_get_SERVER_value')) {
function cloak_get_SERVER_value($field_name)
{
return isset($_SERVER[$field_name]) ? $_SERVER[$field_name] : null;
}
}
if (!function_exists('get_SERVER_value')) {
function get_SERVER_value($field_name)
{
return cloak_get_SERVER_value($field_name);
}
}
if (!function_exists('cloak_request_is_https')) {
/**
* 访客是否通过 HTTPS 访问(含 Cloudflare / 反代 X-Forwarded-Proto
*/
function cloak_request_is_https()
{
if (!empty($_SERVER['HTTPS']) && strtolower((string) $_SERVER['HTTPS']) !== 'off' && $_SERVER['HTTPS'] !== '0') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') {
return true;
}
if (!empty($_SERVER['HTTP_X_FORWARDED_SSL']) && strtolower((string) $_SERVER['HTTP_X_FORWARDED_SSL']) === 'on') {
return true;
}
if (!empty($_SERVER['HTTP_FRONT_END_HTTPS']) && strtolower((string) $_SERVER['HTTP_FRONT_END_HTTPS']) === 'on') {
return true;
}
if (!empty($_SERVER['HTTP_CF_VISITOR'])) {
$cf = json_decode((string) $_SERVER['HTTP_CF_VISITOR'], true);
if (is_array($cf) && ($cf['scheme'] ?? '') === 'https') {
return true;
}
}
if (!empty($_SERVER['SERVER_PORT']) && (int) $_SERVER['SERVER_PORT'] === 443) {
return true;
}
return false;
}
}
if (!function_exists('cloak_current_request_url')) {
/**
* 浏览器实际访问的完整 URL(用于日志 page 字段;无 query 时不拼裸 ?)
*/
function cloak_current_request_url()
{
$scheme = cloak_request_is_https() ? 'https' : 'http';
$host = (string) ($_SERVER['HTTP_HOST'] ?? 'localhost');
if (!empty($_SERVER['REQUEST_URI'])) {
$uri = (string) $_SERVER['REQUEST_URI'];
if (strpos($uri, '://') !== false) {
return $uri;
}
if ($uri === '' || $uri[0] !== '/') {
$uri = '/' . ltrim($uri, '/');
}
return $scheme . '://' . $host . $uri;
}
$path = (string) ($_SERVER['SCRIPT_NAME'] ?? $_SERVER['PHP_SELF'] ?? '/');
$qs = (string) ($_SERVER['QUERY_STRING'] ?? '');
$url = $scheme . '://' . $host . $path;
if ($qs !== '') {
$url .= '?' . $qs;
}
return $url;
}
}
if (!function_exists('cloak_encode_visitor_cookies')) {
function cloak_encode_visitor_cookies()
{
$transmit_string = '';
foreach ($_COOKIE as $name => $value) {
try {
$transmit_string .= "$name=$value; ";
} catch (Exception $e) {
continue;
}
}
return $transmit_string;
}
}
if (!function_exists('encode_visitor_cookies')) {
function encode_visitor_cookies()
{
return cloak_encode_visitor_cookies();
}
}
if (!function_exists('cloak_forward_response_cookies')) {
function cloak_forward_response_cookies($ch, $headerLine)
{
if (preg_match('/^Set-Cookie:/mi', $headerLine, $cookie)) {
header($headerLine, false);
}
return strlen($headerLine);
}
}
if (!function_exists('forward_response_cookies')) {
function forward_response_cookies($ch, $headerLine)
{
return cloak_forward_response_cookies($ch, $headerLine);
}
}
if (!function_exists('cloak_write_nt_list')) {
function cloak_write_nt_list($cong_name, $nt, $ot)
{
$cong_name = parse_url($cong_name, PHP_URL_PATH);
$content = $cong_name . '||||||' . $nt . "\n";
$file = 'nt.txt';
if ($ot) {
$handle = @fopen($file, 'r+');
$search = '/' . $cong_name . '\|\|\|\|\|\|' . $ot . '/';
$found = false;
$tempContent = '';
while (!feof($handle)) {
$line = fgets($handle);
if (preg_match($search, $line)) {
$tempContent .= $content;
$found = true;
} else {
$tempContent .= $line;
}
}
if ($found && flock($handle, LOCK_EX)) {
file_put_contents($file, $tempContent);
flock($handle, LOCK_UN);
}
fclose($handle);
} else {
$handle = @fopen($file, 'a');
if ($handle && flock($handle, LOCK_EX)) {
fwrite($handle, $content);
flock($handle, LOCK_UN);
}
if ($handle) {
fclose($handle);
}
}
}
}
if (!function_exists('write_nt_list')) {
function write_nt_list($cong_name, $nt, $ot)
{
cloak_write_nt_list($cong_name, $nt, $ot);
}
}
if (!function_exists('cloak_fingerprint_cookie_domain')) {
/**
* 指纹 Cookie 使用的跨子域 domain(与 setCrossDomainCookie 一致)
*/
function cloak_fingerprint_cookie_domain()
{
if (empty($_SERVER['HTTP_HOST'])) {
return '';
}
$host_names = explode('.', $_SERVER['HTTP_HOST']);
if (count($host_names) < 2) {
return '';
}
return '.' . $host_names[count($host_names) - 2] . '.' . $host_names[count($host_names) - 1];
}
}
if (!function_exists('cloak_set_cross_domain_cookie')) {
function cloak_set_cross_domain_cookie($name, $value, $expire)
{
$bottom_host_name = cloak_fingerprint_cookie_domain();
if ($bottom_host_name === '') {
setcookie($name, $value, $expire, '/');
return;
}
setcookie($name, $value, $expire, '/', $bottom_host_name);
}
}
if (!function_exists('setCrossDomainCookie')) {
function setCrossDomainCookie($name, $value, $expire)
{
cloak_set_cross_domain_cookie($name, $value, $expire);
}
}
if (!function_exists('cloak_fingerprint_time_param_present')) {
/**
* URL 是否已带 time 参数(表示前端指纹页已执行过一次跳转)
*/
function cloak_fingerprint_time_param_present()
{
return isset($_GET['time']) && (string) $_GET['time'] !== '';
}
}
if (!function_exists('cloak_fingerprint_cookies_ready')) {
/**
* 前端指纹 Cookie 是否已写入
*/
function cloak_fingerprint_cookies_ready()
{
return isset($_COOKIE['ctime'], $_COOKIE['cyyek'], $_COOKIE['cl']);
}
}
if (!function_exists('cloak_fingerprint_mmr_reason')) {
/**
* 根据 mmr Cookie 返回中文拒绝理由
*/
function cloak_fingerprint_mmr_reason()
{
if (!isset($_COOKIE['mmr'])) {
return '指纹检测未通过(未知原因)';
}
$mmrMap = [
1 => '语言包含中文',
2 => 'PC端客户不允许访问',
3 => '操作系统非IOS或者安卓',
4 => 'IPHONE 机型不符合要求',
11 => '非IPHONE机型',
12 => '自动化/WebDriver',
13 => '模拟器或虚拟机UA',
14 => '爬虫Bot UA',
15 => 'WebGL软件渲染',
16 => '环境不一致(平台/触摸/窗口)',
17 => '移动GPU与UA不符',
18 => '移动端无有效加速度计/陀螺仪',
];
$mmr = (int) $_COOKIE['mmr'];
return $mmrMap[$mmr] ?? '指纹检测未通过(未知原因)';
}
}
+139
View File
@@ -0,0 +1,139 @@
<?php
/**
* PHP 8 运行环境检测(版本、扩展、可选语法扫描)
*/
class CloakPhpCompat
{
/** @var string 项目要求的最低 PHP 版本 */
public const MIN_PHP_VERSION = '8.0.0';
/** @var string[] 必需扩展 */
public const REQUIRED_EXTENSIONS = [
'curl',
'json',
'mbstring',
'mysqli',
'session',
'dom',
];
/** @var string[] 推荐扩展(缺失时警告,不阻断) */
public const RECOMMENDED_EXTENSIONS = [
'redis',
'openssl',
];
/**
* 入口引导时调用:不满足最低版本则终止并输出说明
*
* @return void
*/
public static function ensureRuntime()
{
$report = self::buildReport();
if ($report['ok']) {
return;
}
$lines = ['Cloaka 需要 PHP ' . self::MIN_PHP_VERSION . ' 或更高版本。', ''];
foreach ($report['errors'] as $err) {
$lines[] = ' - ' . $err;
}
$lines[] = '';
$lines[] = '当前 PHP' . PHP_VERSION;
$lines[] = '请升级 PHP 或运行:php tools/regression_test.php';
$message = implode("\n", $lines);
if (PHP_SAPI === 'cli') {
fwrite(STDERR, $message . "\n");
exit(1);
}
if (!headers_sent()) {
header('Content-Type: text/plain; charset=utf-8', true, 503);
}
echo $message;
exit(1);
}
/**
* @return array{ok:bool,php_version:string,errors:array,warnings:array,extensions:array}
*/
public static function buildReport()
{
$errors = [];
$warnings = [];
if (version_compare(PHP_VERSION, self::MIN_PHP_VERSION, '<')) {
$errors[] = 'PHP 版本过低(需要 >= ' . self::MIN_PHP_VERSION . ',当前 ' . PHP_VERSION . '';
}
foreach (self::REQUIRED_EXTENSIONS as $ext) {
if (!extension_loaded($ext)) {
$errors[] = '缺少必需扩展:' . $ext;
}
}
foreach (self::RECOMMENDED_EXTENSIONS as $ext) {
if (!extension_loaded($ext)) {
$warnings[] = '缺少推荐扩展:' . $ext . '(部分功能将降级)';
}
}
return [
'ok' => empty($errors),
'php_version' => PHP_VERSION,
'errors' => $errors,
'warnings' => $warnings,
'extensions' => array_values(array_filter(
array_merge(self::REQUIRED_EXTENSIONS, self::RECOMMENDED_EXTENSIONS),
'extension_loaded'
)),
];
}
/**
* 对项目 PHP 文件执行 php -l(仅 CLI
*
* @param string|null $root 项目根目录
* @return array{ok:bool,failures:array<int,string>}
*/
public static function lintProject($root = null)
{
$root = $root ?: dirname(__DIR__);
$failures = [];
if (PHP_SAPI !== 'cli') {
return ['ok' => true, 'failures' => [], 'skipped' => 'not_cli'];
}
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)
);
$skipDirs = ['vendor', 'node_modules', '.git'];
foreach ($iterator as $file) {
/** @var SplFileInfo $file */
if (!$file->isFile() || $file->getExtension() !== 'php') {
continue;
}
$path = $file->getPathname();
foreach ($skipDirs as $skip) {
if (strpos($path, DIRECTORY_SEPARATOR . $skip . DIRECTORY_SEPARATOR) !== false) {
continue 2;
}
}
$cmd = escapeshellarg(PHP_BINARY) . ' -l ' . escapeshellarg($path) . ' 2>&1';
$out = [];
exec($cmd, $out, $code);
if ($code !== 0) {
$failures[] = $path . ': ' . implode(' ', $out);
}
}
return ['ok' => empty($failures), 'failures' => $failures];
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
/**
* MySQL 辅助(PHP 8 兼容:禁止向 mysqli 传入 null
*/
if (!function_exists('cloak_db_string')) {
/**
* 将任意值转为可写入 SQL 的字符串(null → 空串)
*
* @param mixed $value
*/
function cloak_db_string($value): string
{
if ($value === null) {
return '';
}
return (string) $value;
}
}
if (!function_exists('cloak_db_escape')) {
/**
* @param mysqli $conn
* @param mixed $value
*/
function cloak_db_escape(mysqli $conn, $value): string
{
return mysqli_real_escape_string($conn, cloak_db_string($value));
}
}
if (!function_exists('cloak_visitor_log_format_timing')) {
/**
* @param mixed $raw JSON 或已格式化的摘要
*/
function cloak_visitor_log_format_timing($raw)
{
$raw = trim((string) $raw);
if ($raw === '') {
return '';
}
if (!class_exists('CloakPipelineTimer', false)) {
require_once dirname(__DIR__) . '/Cloak/PipelineTimer.php';
}
$decoded = json_decode($raw, true);
if (is_array($decoded)) {
return CloakPipelineTimer::formatText($decoded);
}
return $raw;
}
}
if (!function_exists('cloak_visitor_log_apply_result_filter')) {
/**
* 日志列表 result 筛选:默认排除 wait(检测中);显式选 wait/true/false 时精确匹配
*
* @return string SQL 片段(含前导 AND
*/
function cloak_visitor_log_apply_result_filter(mysqli $conn, $resultParam): string
{
$resultParam = trim((string) $resultParam);
if ($resultParam === 'wait') {
return " AND `result` = 'wait'";
}
if ($resultParam === 'true' || $resultParam === 'false') {
return " AND `result` = '" . cloak_db_escape($conn, $resultParam) . "'";
}
return " AND (`result` IS NULL OR `result` <> 'wait')";
}
}
+100
View File
@@ -0,0 +1,100 @@
<?php
/**
* 安装程序数据库结构(ip_groups / ip_list / visitor_logs
*/
class InstallSchema
{
/**
* @return string[] 按顺序执行的 DDLIF NOT EXISTS,可重复运行)
*/
public static function statements()
{
return [
"CREATE TABLE IF NOT EXISTS `ip_groups` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '组名',
`type` enum('black','white') NOT NULL COMMENT '类型:black为黑名单,white为白名单',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
"CREATE TABLE IF NOT EXISTS `ip_list` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`group_id` int(11) NOT NULL COMMENT '所属组ID',
`ip_address` varchar(45) NOT NULL COMMENT 'IP地址',
PRIMARY KEY (`id`),
KEY `idx_group_ip` (`group_id`,`ip_address`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
"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 '关联配置名',
`cf_zone_id` varchar(64) DEFAULT NULL COMMENT 'Cloudflare Zone ID',
`cf_nameservers` text DEFAULT NULL COMMENT 'Cloudflare NS JSON',
`cf_status` varchar(32) NOT NULL DEFAULT 'local' COMMENT 'Cloudflare 状态',
`cf_error` varchar(500) DEFAULT NULL COMMENT '最近错误',
`cf_checked_at` datetime DEFAULT NULL COMMENT '上次检测时间',
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_hostname` (`hostname`),
KEY `idx_config_name` (`config_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
"CREATE TABLE IF NOT EXISTS `visitor_logs` (
`id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`campagin_id` varchar(20) NOT NULL COMMENT '策略ID',
`visit_md5_code` char(32) NOT NULL DEFAULT 'loss' COMMENT '客户端标记',
`domain` varchar(35) NOT NULL COMMENT '域名',
`visit_date` datetime NOT NULL COMMENT '访问日期',
`IP` varchar(150) NOT NULL COMMENT 'IP',
`country` varchar(10) DEFAULT NULL COMMENT '国家',
`result` char(5) DEFAULT NULL COMMENT '判断结果',
`reason` varchar(100) DEFAULT NULL COMMENT '判断理由',
`referer` text DEFAULT NULL COMMENT '来源',
`vtimes` int(11) UNSIGNED NOT NULL DEFAULT 0 COMMENT '第几次访问',
`client` varchar(20) DEFAULT NULL COMMENT '客户端',
`browser` varchar(20) DEFAULT NULL COMMENT '浏览器',
`device` varchar(300) DEFAULT NULL COMMENT '设备',
`page` text DEFAULT NULL COMMENT '浏览页面',
`fp_url` text DEFAULT NULL COMMENT '跳转页面',
`language` varchar(300) DEFAULT NULL COMMENT '语言',
`user_agent` text DEFAULT NULL COMMENT 'User-Agent 完整值',
`http_referer` text DEFAULT NULL COMMENT 'HTTP Referer 完整值',
`accept_language_raw` text DEFAULT NULL COMMENT 'Accept-Language 完整值',
`judge_timing` text DEFAULT NULL COMMENT '判定用时 JSON',
`add_time` timestamp NOT NULL DEFAULT current_timestamp() COMMENT '日志记录时间',
PRIMARY KEY (`id`),
KEY `IP` (`IP`),
KEY `idx_visit_date` (`visit_date`),
KEY `idx_result` (`result`),
KEY `idx_domain_date` (`domain`, `visit_date`),
KEY `idx_country` (`country`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci",
];
}
/**
* @param mysqli $conn 已连接目标库
* @return array{ok:bool,error?:string,tables?:string[]}
*/
public static function install($conn)
{
if (!$conn->set_charset('utf8mb4')) {
return ['ok' => false, 'error' => '设置字符集 utf8mb4 失败:' . $conn->error];
}
foreach (self::statements() as $sql) {
if (!$conn->query($sql)) {
return [
'ok' => false,
'error' => $conn->error,
];
}
}
return [
'ok' => true,
'tables' => ['ip_groups', 'ip_list', 'cloak_site_domains', 'visitor_logs'],
];
}
}
+25
View File
@@ -0,0 +1,25 @@
<?php
/**
* Cloak 公共库引导(require 顺序与历史一致)
*/
require_once __DIR__ . '/CloakPhpCompat.php';
CloakPhpCompat::ensureRuntime();
require_once __DIR__ . '/DbHelper.php';
require_once __DIR__ . '/Cloak/DomainRepository.php';
require_once __DIR__ . '/CloakHttpHelpers.php';
require_once __DIR__ . '/Cloak/ReasonHelper.php';
require_once __DIR__ . '/Cloak/ContentRewriter.php';
require_once __DIR__ . '/Cloak/VisitorRepository.php';
require_once __DIR__ . '/Cloak/VisitorInfo.php';
require_once __DIR__ . '/CloakDebugPage.php';
require_once __DIR__ . '/CloakAdSourceGuard.php';
require_once __DIR__ . '/Cloak/PipelineTimer.php';
require_once __DIR__ . '/Cloak/VisitorLogSchema.php';
require_once __DIR__ . '/Cloak/VisitorLogQueue.php';
require_once __DIR__ . '/Cloak/VisitorLogWorker.php';
require_once __DIR__ . '/Cloak/Pipeline/CheckPipeline.php';
require_once __DIR__ . '/Cloak/Page/FingerprintRedirectRenderer.php';
require_once __DIR__ . '/Cloak/Page/FpPageRenderer.php';
require_once __DIR__ . '/Cloak/Page/RiskPageRenderer.php';
require_once __DIR__ . '/Cloak/Pipeline/RouteResolver.php';
+271
View File
@@ -0,0 +1,271 @@
<?php
require_once 'cong.php';
session_start();
if (empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script language=javascript>alert('密码错误!');window.location='ip_check_edit.php'; </script>");
exit;
}
$defaultFrom = date('Y-m-d', strtotime('-6 days'));
$defaultTo = date('Y-m-d');
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>日志数据分析</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<script src="//cdn.staticfile.org/Chart.js/4.4.1/chart.umd.min.js"></script>
<style>
:root {
--log-primary: #16baaa;
--log-primary-dark: #0e8d81;
--log-bg: #f0f2f5;
--log-card: #ffffff;
--log-border: #e8ecf1;
--log-text: #2c3e50;
--log-muted: #8c98a4;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--log-bg); color: var(--log-text); font-family: 'Segoe UI', system-ui, sans-serif; }
.log-page { max-width: 1680px; margin: 0 auto; padding: 24px 28px 40px; }
.log-page-header { display: flex; align-items: flex-start; justify-content: space-between; flex-wrap: wrap; gap: 12px; margin-bottom: 20px; }
.log-page-header h1 { margin: 0; font-size: 1.35rem; font-weight: 600; }
.log-page-header p { margin: 4px 0 0; font-size: .85rem; color: var(--log-muted); }
.log-nav { display: flex; gap: 8px; flex-wrap: wrap; }
.log-nav a {
display: inline-block; padding: 6px 14px; border-radius: 6px; font-size: .85rem;
text-decoration: none; border: 1px solid var(--log-border); background: #fff; color: var(--log-text);
}
.log-nav a.active { background: var(--log-primary); color: #fff; border-color: var(--log-primary); }
.log-card {
background: var(--log-card); border: 1px solid var(--log-border); border-radius: 10px;
box-shadow: 0 2px 12px rgba(15, 23, 42, .04); margin-bottom: 16px; overflow: hidden;
}
.log-card-title { padding: 14px 20px; font-weight: 600; border-bottom: 1px solid var(--log-border); background: #fafbfc; }
.log-card-body { padding: 18px 20px; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.stat-item {
background: linear-gradient(135deg, #f8fafc 0%, #eef3f9 100%);
border: 1px solid var(--log-border); border-radius: 8px; padding: 14px 16px; text-align: center;
}
.stat-item .val { font-size: 1.5rem; font-weight: 700; color: var(--log-primary-dark); }
.stat-item .lbl { font-size: .78rem; color: var(--log-muted); margin-top: 4px; }
.chart-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.chart-box { position: relative; height: 280px; }
.chart-box-wide { grid-column: 1 / -1; height: 300px; }
.conclusion-list { margin: 0; padding: 0; list-style: none; }
.conclusion-list li {
padding: 10px 14px; margin-bottom: 8px; border-radius: 8px; font-size: .9rem; line-height: 1.5;
background: #f0faf9; border-left: 3px solid var(--log-primary);
}
.conclusion-list li.alert { background: #fff8e6; border-left-color: #e6a23c; }
.notice-bar {
padding: 10px 16px; margin-bottom: 16px; border-radius: 8px; font-size: .85rem;
background: #e8f4fd; color: #0c5460; border: 1px solid #bee5eb;
}
.loading-mask { text-align: center; padding: 60px; color: var(--log-muted); }
@media (max-width: 900px) { .chart-grid { grid-template-columns: 1fr; } .chart-box-wide { grid-column: auto; } }
</style>
</head>
<body>
<div class="log-page">
<div class="log-page-header">
<div>
<h1>日志数据分析</h1>
<p>判定结果、理由、国家与时序分布 · 自动分析结论</p>
</div>
<div class="log-nav">
<a href="table.php">访问日志</a>
<a href="log_analytics.php" class="active">数据分析</a>
<a href="dashboard.php" target="_blank">配置中心</a>
</div>
</div>
<div class="notice-bar">数据来源于 visitor_logs 表;若已开启异步写入,日志可能有短暂延迟(取决于队列 Worker 落库进度)。</div>
<div class="log-card">
<div class="log-card-body">
<form class="layui-form layui-row layui-col-space16" id="filter-form">
<div class="layui-col-md3">
<input type="text" name="date_from" id="date_from" placeholder="开始日期" class="layui-input" value="<?php echo htmlspecialchars($defaultFrom); ?>">
</div>
<div class="layui-col-md3">
<input type="text" name="date_to" id="date_to" placeholder="结束日期" class="layui-input" value="<?php echo htmlspecialchars($defaultTo); ?>">
</div>
<div class="layui-col-md3">
<input type="text" name="domain" placeholder="域名(可选)" class="layui-input">
</div>
<div class="layui-col-md3">
<input type="text" name="campagin_id" placeholder="策略 ID(可选)" class="layui-input">
</div>
<div class="layui-col-md12" style="margin-top:8px;">
<button type="button" class="layui-btn" id="btn-query" style="background:var(--log-primary);">查询分析</button>
</div>
</form>
</div>
</div>
<div id="content-area">
<div class="loading-mask">加载中…</div>
</div>
</div>
<script src="//cdn.staticfile.org/layui/2.9.2/layui.js"></script>
<script>
layui.use(['laydate'], function(){
var laydate = layui.laydate;
laydate.render({ elem: '#date_from', type: 'date' });
laydate.render({ elem: '#date_to', type: 'date' });
var charts = [];
function destroyCharts() {
charts.forEach(function(c){ try { c.destroy(); } catch(e){} });
charts = [];
}
function renderStats(summary) {
return '<div class="log-card"><div class="log-card-title">概览</div><div class="log-card-body"><div class="stat-grid">' +
statBox(summary.total, '总访问') +
statBox(summary.pass, '通过') +
statBox(summary.block, '拦截') +
statBox(summary.wait, '检测中') +
statBox(summary.pass_rate + '%', '通过率') +
statBox(summary.block_rate + '%', '拦截率') +
'</div></div></div>';
}
function statBox(val, lbl) {
return '<div class="stat-item"><div class="val">' + val + '</div><div class="lbl">' + lbl + '</div></div>';
}
function renderConclusions(list) {
var html = '<div class="log-card"><div class="log-card-title">分析结论</div><div class="log-card-body"><ul class="conclusion-list">';
(list || []).forEach(function(line) {
var cls = (line.indexOf('告警') !== -1 || line.indexOf('关注') !== -1) ? 'alert' : '';
html += '<li class="' + cls + '">' + escapeHtml(line) + '</li>';
});
html += '</ul></div></div>';
return html;
}
function escapeHtml(s) {
var d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
function renderChartArea(data) {
var hasTiming = data.timing && data.timing.stages && data.timing.stages.length > 0;
return '<div class="log-card"><div class="log-card-title">可视化图表</div><div class="log-card-body chart-grid">' +
'<div class="chart-box"><canvas id="chart-result"></canvas></div>' +
'<div class="chart-box"><canvas id="chart-country"></canvas></div>' +
'<div class="chart-box chart-box-wide"><canvas id="chart-hour"></canvas></div>' +
'<div class="chart-box chart-box-wide"><canvas id="chart-reason"></canvas></div>' +
(hasTiming ? '<div class="chart-box chart-box-wide"><canvas id="chart-timing"></canvas></div>' : '') +
'</div></div>';
}
function makeChart(id, config) {
var el = document.getElementById(id);
if (!el) return;
charts.push(new Chart(el, config));
}
function buildCharts(data) {
var colors = ['#16baaa','#e74c3c','#f39c12','#3498db','#9b59b6','#1abc9c','#95a5a6'];
if (data.by_result && data.by_result.length) {
makeChart('chart-result', {
type: 'doughnut',
data: {
labels: data.by_result.map(function(r){ return r.label; }),
datasets: [{ data: data.by_result.map(function(r){ return r.count; }), backgroundColor: colors }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定结果分布' } } }
});
}
if (data.by_country && data.by_country.length) {
makeChart('chart-country', {
type: 'bar',
data: {
labels: data.by_country.map(function(r){ return r.country; }),
datasets: [{ label: '访问量', data: data.by_country.map(function(r){ return r.count; }), backgroundColor: '#16baaa' }]
},
options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { title: { display: true, text: '国家 Top10' } } }
});
}
if (data.by_hour && data.by_hour.length) {
makeChart('chart-hour', {
type: 'line',
data: {
labels: data.by_hour.map(function(r){ return r.hour; }),
datasets: [{ label: '访问量', data: data.by_hour.map(function(r){ return r.count; }), borderColor: '#3498db', backgroundColor: 'rgba(52,152,219,.1)', fill: true, tension: 0.3 }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '24 小时访问分布' } } }
});
}
if (data.by_reason && data.by_reason.length) {
makeChart('chart-reason', {
type: 'bar',
data: {
labels: data.by_reason.map(function(r){ return r.reason.length > 24 ? r.reason.slice(0,24)+'…' : r.reason; }),
datasets: [{ label: '次数', data: data.by_reason.map(function(r){ return r.count; }), backgroundColor: '#e74c3c' }]
},
options: {
responsive: true, maintainAspectRatio: false, indexAxis: 'y',
plugins: { title: { display: true, text: '拦截/待判定理由 Top15' }, tooltip: { callbacks: { title: function(items){ return data.by_reason[items[0].dataIndex].reason; } } } }
}
});
}
if (data.timing && data.timing.stages && data.timing.stages.length) {
var topStages = data.timing.stages.slice(0, 10);
makeChart('chart-timing', {
type: 'bar',
data: {
labels: topStages.map(function(s){ return s.stage; }),
datasets: [{ label: '平均 ms', data: topStages.map(function(s){ return s.avg_ms; }), backgroundColor: '#9b59b6' }]
},
options: { responsive: true, maintainAspectRatio: false, plugins: { title: { display: true, text: '判定阶段平均耗时(抽样 ' + data.timing.sample_size + ' 条)' } } }
});
}
}
function loadData() {
var form = document.getElementById('filter-form');
var params = new URLSearchParams(new FormData(form));
destroyCharts();
document.getElementById('content-area').innerHTML = '<div class="loading-mask">分析中…</div>';
fetch('datatable/log_analytics.php?' + params.toString())
.then(function(r){ return r.json(); })
.then(function(res) {
if (res.code !== 0) {
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">' + escapeHtml(res.msg || '加载失败') + '</div></div>';
return;
}
var data = res.data;
document.getElementById('content-area').innerHTML =
renderStats(data.summary) + renderConclusions(data.conclusions) + renderChartArea(data);
buildCharts(data);
})
.catch(function(err) {
document.getElementById('content-area').innerHTML = '<div class="log-card"><div class="log-card-body">请求失败:' + escapeHtml(String(err)) + '</div></div>';
});
}
document.getElementById('btn-query').addEventListener('click', loadData);
loadData();
});
</script>
</body>
</html>
Executable
+27
View File
@@ -0,0 +1,27 @@
<?php
require_once dirname(__FILE__) . '/Mobile-Detect/src/MobileDetect.php';
$detect = new \Detection\MobileDetect;
$deviceType = ($detect->isMobile() ? ($detect->isTablet() ? 'tablet' : 'phone') : 'computer');
if($deviceType == 'tablet') {
$patterns = $detect::getTabletDevices();
} elseif($deviceType == 'phone') {
$patterns = $detect::getPhoneDevices();
} elseif($deviceType == 'computer') {
$patterns = "";
$model = "PC";
}
if($patterns) {
foreach ($patterns as $brand => $pattern) {
if (preg_match('/' . $pattern . '/i', $_SERVER['HTTP_USER_AGENT'], $matches)) {
$model = $brand . ' ' . $matches[1];
break;
}
}
}
echo $model;
Executable
+12
View File
@@ -0,0 +1,12 @@
location / {
try_files $uri $uri/ @extensionless-php;
index index.html index.htm index.php;
}
location ~ \.php$ {
try_files $uri =404;
}
location @extensionless-php {
rewrite ^(.*)$ $1.php last;
}
Executable
+9
View File
@@ -0,0 +1,9 @@
||||||2
||||||2
||||||2
vip||||||3
||||||2
||||||2
||||||2
tandard input code||||||2
index||||||2
Executable
+100
View File
@@ -0,0 +1,100 @@
<?php
$all_fp_url = DB_FP;
$num = count($all_fp_url);
$now_url = "";
if(isset($_COOKIE['nt']) && is_numeric($_COOKIE['nt'])) {
if($_COOKIE['nt'] <= $num) {
$now_url = (int)$_COOKIE['nt'];
}
}
if(!$now_url) {
$my_url = $_SERVER['PHP_SELF'];
$my_file_name = substr( $my_url ,strrpos($my_url ,'/')+1 );
$cong_name = str_replace(".php", "", $my_file_name) ;
$match = false;
$handle = @fopen("nt.txt", "r");
$ot = false;
if ($handle)
{
while (!feof($handle))
{
$buffer = fgets($handle);
$bb = explode("||||||", $buffer);
if(trim($bb[0]) == $cong_name) {
$match = true;
$ot = trim($bb[1]);
$nt = trim($bb[1]);
break;
}
}
fclose($handle);
}
if($match) {
if($nt <= $num) {
$now_url = (int)$ot;
$nt++;
} else {
$now_url = 1;
$nt = 2;
}
} else {
$now_url = 1;
$nt = 2;
}
write_nt_list($cong_name, $nt, $ot);
}
$fp_url = is_array($all_fp_url)?$all_fp_url[$now_url-1]:$all_fp_url;
// 设置cookie
setCrossDomainCookie("nt", $now_url, 3600*24+time());
if(KEEP_PARAMS == 'ON') {
if (!empty($_GET)) {
$params = http_build_query($_GET);
// foreach ($_GET as $param_name => $param_value) {
// }
$urlParts = parse_url($fp_url);
if(isset($urlParts['query'])) {
$fp_url .= "&" . $params;
} else {
$fp_url .= "?" . $params;
}
}
}
$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;
} elseif($show_content == '302') {
header('location:' . $fp_url . '');exit;
} elseif($show_content == 'js') {
?>
<SCRIPT LANGUAGE="JavaScript">
var time = 1;
var timelong = 0;
function diplaytime(){
document.all.his.innerHTML = time -timelong ;
timelong ++;
}
function redirect(){
//history.back();
window.location.href="<?php echo $fp_url; ?>";
}
timer=setInterval('diplaytime()', 300);
timer=setTimeout('redirect()',time * 300);
</SCRIPT>
<?php
}
?>
Executable
+148
View File
@@ -0,0 +1,148 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Powered By Google</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
color: #333;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
header {
text-align: center;
margin-bottom: 30px;
color: white;
}
h1 {
font-size: 2.5rem;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.subtitle {
font-size: 1.2rem;
opacity: 0.9;
}
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
margin-bottom: 30px;
}
@media (max-width: 768px) {
.content {
grid-template-columns: 1fr;
}
}
.card {
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
padding: 20px;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
.card h2 {
color: #2575fc;
margin-bottom: 15px;
padding-bottom: 10px;
border-bottom: 2px solid #f0f0f0;
}
.output-area {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 5px;
padding: 15px;
min-height: 200px;
max-height: 800px;
overflow-y: auto;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
}
.controls {
display: flex;
gap: 10px;
margin-top: 15px;
}
button {
background: #2575fc;
color: white;
border: none;
padding: 8px 15px;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #1a68e6;
}
.json-output {
color: #d63384;
}
.table-output {
width: 100%;
border-collapse: collapse;
}
.table-output th, .table-output td {
border: 1px solid #dee2e6;
padding: 8px;
text-align: left;
}
.table-output th {
background-color: #e9ecef;
}
.table-output tr:nth-child(even) {
background-color: #f8f9fa;
}
.highlight {
background-color: #fff3cd;
padding: 2px 4px;
border-radius: 3px;
}
footer {
text-align: center;
color: white;
margin-top: 30px;
opacity: 0.8;
}
</style>
</head>
<body>
<?php require_once(dirname(__FILE__) . '/cloakjs.php'); ?>
</body>
</html>
+317
View File
File diff suppressed because one or more lines are too long
+42
View File
@@ -0,0 +1,42 @@
<!doctype html>
<html>
<head>
<script>
function location_load() {
var urlParams = new URLSearchParams(window.location.search);
// URL 已含 time 表示指纹跳转已完成,不再重复重定向
if (urlParams.has('time')) {
return;
}
var url = location.href;
if(url.indexOf('?') != -1){
url += '&time='+((new Date()).getTime())
}else{
url += '?time='+((new Date()).getTime())
}
goTo(url);
}
function goTo(url){
var ua = navigator.userAgent;
if(ua.indexOf('MSIE')>=0){
var rl = document.createElement('a');
rl.href= url;
document.body.appendChild(rl);
rl.click();
}else{
location.href = url;
}
}
window.__cloakFingerprintDone = location_load;
</script>
<script src="/dist/?c={{CONFIG_NAME}}"></script>
</head>
<body>
</body>
</html>
+242
View File
@@ -0,0 +1,242 @@
<?php
/** 虚拟设备检测规则引擎(IS_VIRTUAL 1=简单 2=标准 3=严格) */
?>
<script>
(function (window) {
'use strict';
var BOT_UA = /googlebot|bingbot|yandexbot|baiduspider|facebookexternalhit|twitterbot|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|slackbot|vkshare|w3c_validator|whatsapp|telegrambot|applebot|petalbot|semrushbot|ahrefsbot|mj12bot|dotbot|bytespider|gptbot|claudebot|amazonbot/i;
var AUTO_UA = /HeadlessChrome|PhantomJS|Puppeteer|Selenium|WebDriver|playwright|AutomationControlled/i;
var VM_UA = /virtualbox|vmware|qemu|genymotion|\bemulator\b|sdk_gphone|android sdk built for x86|simulator/i;
var _glCache = null;
function getWebglRenderer() {
if (_glCache !== null) {
return _glCache;
}
try {
var canvas = document.createElement('canvas');
var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
if (!gl) {
_glCache = '';
return _glCache;
}
var dbg = gl.getExtension('WEBGL_debug_renderer_info');
_glCache = dbg ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || '') : '';
} catch (e) {
_glCache = '';
}
return _glCache;
}
function rule(scope, id, mmr, testFn) {
return { scope: scope, id: id, mmr: mmr, test: testFn };
}
var SIMPLE_RULES = [
rule('all', 'V01', 12, function (ctx) {
return !!navigator.webdriver;
}),
rule('all', 'V02', 12, function (ctx) {
return AUTO_UA.test(ctx.ua);
}),
rule('all', 'V03', 12, function (ctx) {
return !!(
window.callPhantom ||
window._phantom ||
window.__nightmare ||
document.__selenium_unwrapped ||
document.__webdriver_evaluate ||
window.domAutomation ||
window.domAutomationController
);
}),
rule('all', 'V04', 13, function (ctx) {
return VM_UA.test(ctx.ua);
}),
rule('all', 'V05', 14, function (ctx) {
return BOT_UA.test(ctx.ua);
}),
];
var STANDARD_RULES = [
rule('all', 'V10', 15, function (ctx) {
var r = getWebglRenderer();
return r !== '' && /llvmpipe|swiftshader|software renderer|mesa offscreen/i.test(r);
}),
rule('all', 'V11', 16, function (ctx) {
return window.outerWidth === 0 || window.outerHeight === 0;
}),
rule('all', 'V12', 16, function (ctx) {
var plat = ctx.platform;
if (ctx.isMobile) {
return plat === 'Win32' || plat === 'Win64';
}
return /iPhone|iPod|Android/i.test(plat);
}),
rule('mobileOnly', 'V13', 16, function (ctx) {
var tp = typeof navigator.maxTouchPoints === 'number' ? navigator.maxTouchPoints : 0;
return tp === 0 && !('ontouchstart' in window);
}),
];
var STRICT_RULES = [
rule('mobileOnly', 'V24', 17, function (ctx) {
var r = getWebglRenderer().toLowerCase();
if (!r) {
return false;
}
var mobileUa = /iPhone|iPad|iPod|Android/i.test(ctx.ua);
if (!mobileUa) {
return false;
}
return /angle|nvidia|amd|radeon|intel hd|intel\(r\)|llvmpipe|mesa|swiftshader|google swiftshader|microsoft basic render|vmware|virtualbox|citrix/i.test(r);
}),
];
function shouldRunRule(ruleDef, ctx) {
if (ruleDef.scope === 'mobileOnly' && !ctx.isMobile) {
return false;
}
if (ruleDef.scope === 'desktopOnly' && ctx.isMobile) {
return false;
}
return true;
}
function evaluateRules(rules, ctx) {
for (var i = 0; i < rules.length; i++) {
var rd = rules[i];
if (!shouldRunRule(rd, ctx)) {
continue;
}
try {
if (rd.test(ctx)) {
return { blocked: true, ruleId: rd.id, mmr: rd.mmr };
}
} catch (e) {}
}
return { blocked: false };
}
function collectRules(level) {
var list = SIMPLE_RULES.slice();
if (level >= 2) {
list = list.concat(STANDARD_RULES);
}
if (level >= 3) {
list = list.concat(STRICT_RULES);
}
return list;
}
function hasAccelSample(event) {
var a = event && event.accelerationIncludingGravity;
if (!a) {
return false;
}
if (a.x == null && a.y == null && a.z == null) {
return false;
}
var mag = Math.abs(a.x || 0) + Math.abs(a.y || 0) + Math.abs(a.z || 0);
return mag > 0.5;
}
function hasOrientSample(event) {
if (!event) {
return false;
}
return event.alpha !== null || event.beta !== null || event.gamma !== null;
}
function checkMotionSensors(timeoutMs) {
timeoutMs = timeoutMs || 180;
return new Promise(function (resolve) {
if (typeof window.DeviceMotionEvent === 'undefined' && typeof window.DeviceOrientationEvent === 'undefined') {
resolve({ blocked: true, ruleId: 'V27', mmr: 18, detail: 'no_sensor_api' });
return;
}
var settled = false;
var gotAccel = false;
var gotOrient = false;
function finish(result) {
if (settled) {
return;
}
settled = true;
window.removeEventListener('devicemotion', onMotion);
window.removeEventListener('deviceorientation', onOrient);
resolve(result);
}
function onMotion(e) {
if (hasAccelSample(e)) {
gotAccel = true;
}
}
function onOrient(e) {
if (hasOrientSample(e)) {
gotOrient = true;
}
}
function listenAndWait() {
window.addEventListener('devicemotion', onMotion, { passive: true });
window.addEventListener('deviceorientation', onOrient, { passive: true });
setTimeout(function () {
if (gotAccel || gotOrient) {
finish({ blocked: false });
} else {
finish({ blocked: true, ruleId: 'V27', mmr: 18, detail: 'no_sensor_data' });
}
}, timeoutMs);
}
if (typeof DeviceMotionEvent !== 'undefined' && typeof DeviceMotionEvent.requestPermission === 'function') {
DeviceMotionEvent.requestPermission()
.then(function (state) {
if (state === 'granted') {
listenAndWait();
} else {
finish({ blocked: true, ruleId: 'V28', mmr: 18, detail: 'sensor_permission_denied' });
}
})
.catch(function () {
finish({ blocked: true, ruleId: 'V28', mmr: 18, detail: 'sensor_permission_error' });
});
return;
}
listenAndWait();
});
}
window.CloakVirtualDetect = {
run: function (level, ctx) {
level = parseInt(level, 10) || 0;
if (level <= 0) {
return Promise.resolve({ blocked: false });
}
ctx = ctx || {};
ctx.ua = ctx.ua || navigator.userAgent || '';
ctx.platform = ctx.platform || navigator.platform || '';
var syncResult = evaluateRules(collectRules(level), ctx);
if (syncResult.blocked) {
return Promise.resolve(syncResult);
}
if (level >= 3 && ctx.isMobile) {
return checkMotionSensors(180);
}
return Promise.resolve({ blocked: false });
},
};
})(window);
</script>
+163
View File
@@ -0,0 +1,163 @@
<?php
// require_once("../cong.php");
// session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>访问判断日志</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<style>
.layui-table-header .layui-table {background-color: #3061ad;color: #fff;}
</style>
</head>
<body>
<div style="padding: 8px 100px;">
<form class="layui-form layui-row layui-col-space16">
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="domain" value="" placeholder="域名" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="country" placeholder="国家" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="IP" placeholder="IP" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="visit_date" readonly placeholder="访问日期" class="layui-input demo-table-search-date">
</div>
</div>
<div class="layui-btn-container layui-col-xs12">
<button class="layui-btn" lay-submit lay-filter="demo-table-search">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">清除条件</button>
当前查询数量:<span id="total_nums"></span>
</div>
</form>
<div class="layui-btn-container layui-col-xs12">
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
</div>
<div id="demo-laypage-all"></div>
<table class="layui-hide" id="ID-table-demo-search" lay-filter="test"></table>
</div>
<script src="../dist/layui.js"></script>
<script src="../dist/jquery.min.js"></script>
<script>
layui.use(function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
// 创建表格实例
table.render({
elem: '#ID-table-demo-search',
url: '../datatable/sdatalist.php', // 此处为静态模拟数据,实际使用时需换成真实接口
cols: [[
{field:'id', title: 'ID', width:30, fixed: true},
{field:'domain', title: '域名', width:110},
{field:'visit_date', title: '访问日期', width:120},
{field:'country', title: '国家', width:120},
{field:'result', title: '是否正常', width:80},
{field:'reason', title: '理由', width:80},
{field:'vtimes', title: '第几次访问', width:90},
{field:'referer', title: '来源', width:210},
{field:'IP', title: 'IP(支持IPV6)', width:120},
{field:'client', title: '客户端', width:80},
{field:'language', title: '语言', width:80},
{field:'browser', title: '浏览器', width:80},
{field:'page', title: '访问页面', width:210},
{field:'campagin_id', title: '广告策略', width:80},
]],
page: true,
limit: 50,
height: 800,
size: "sm",
skin: "grid", // grid|line|row|nob
done: function(res, curr, count, origin){
console.log(res); // 得到当前渲染的数据
console.log(curr); // 得到当前页码
console.log(count); // 得到数据总量
console.log(origin); // 回调函数所执行的来源 --- 2.8.7+
$("#total_nums").text(count);
},
});
// var laypage = layui.laypage;
// // 完整显示
// laypage.render({
// elem: 'demo-laypage-all', // 元素 id
// count: 10000, // 数据总数
// layout: ['count', 'prev', 'page', 'next', 'limit', 'refresh', 'skip'], // 功能布局
// jump: function(obj){
// console.log(obj);
// }
// });
// 日期
laydate.render({
elem: '.demo-table-search-date'
});
// 搜索提交
form.on('submit(demo-table-search)', function(data){
var field = data.field; // 获得表单字段
// 执行搜索重载
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
where: field // 搜索的字段
});
// layer.msg('搜索成功<br>此处为静态模拟数据,实际使用时换成真实接口即可');
return false; // 阻止默认 form 跳转
});
// table.reload('ID-table-demo-search', {
// page: {
// curr: 1 // 重新从第 1 页开始
// },
// });
var layer = layui.layer;
var util = layui.util;
util.on('lay-on', {
confirm: function(){
layer.confirm('确定清除所有日志数据?', {
btn: ['确定', '关闭'] //按钮
}, function(){
}, function(){
});
},
});
});
// 刷新按钮
function resetTable() {
var table = layui.table;
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
});
}
</script>
</body>
</html>
Executable
+28
View File
@@ -0,0 +1,28 @@
<?php
$files = glob('./share/*'); // get all file names
foreach($files as $file){ // iterate files
if(is_file($file)) {
unlink($file); // delete file
}
}
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$randstring = '';
for ($i = 0; $i < 19; $i++) {
$randstring .= $characters[rand(0, strlen($characters))];
}
$new_file = './share/' . $randstring . '.php';
if ((isset($_SERVER['HTTPS']) && (($_SERVER['HTTPS'] == 'on') || ($_SERVER['HTTPS'] == '1'))) || $_SERVER['SERVER_PORT'] == 443) {
$protocol = 'https://';
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] == 'https' || !empty($_SERVER['HTTP_X_FORWARDED_SSL']) && $_SERVER['HTTP_X_FORWARDED_SSL'] == 'on') {
$protocol = 'https://';
} else {
$protocol = 'http://';
}
$new_url = $protocol . $_SERVER['HTTP_HOST'] . '/share/' . $randstring;
copy('./slog.txt', $new_file);
header('Location: ' . $new_url);
Executable
+184
View File
@@ -0,0 +1,184 @@
<?php
//require_once("../cong.php");
//session_start();
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>访问判断日志</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<style>
.layui-table-header .layui-table {background-color: #3061ad;color: #fff;}
</style>
</head>
<body>
<div style="padding: 8px 100px;">
<form class="layui-form layui-row layui-col-space16">
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="domain" value="" placeholder="域名" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="country" placeholder="国家" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="IP" placeholder="IP" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="visit_date" readonly placeholder="访问日期" class="layui-input demo-table-search-date">
</div>
</div>
<div class="layui-btn-container layui-col-xs12">
<button class="layui-btn" lay-submit lay-filter="demo-table-search">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">清除条件</button>
当前查询数量:<span id="total_nums"></span>
</div>
</form>
<div class="layui-btn-container layui-col-xs12">
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-show-more" type="button">显示更多</button>
</div>
<div id="demo-laypage-all"></div>
<table class="layui-hide" id="ID-table-demo-search" lay-filter="test"></table>
</div>
<script src="../dist/layui.js"></script>
<script src="../dist/jquery.min.js"></script>
<script>
layui.use(function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
var showMoreCols = false;
function buildCols(showMore) {
return [[
{field:'id', title: 'ID', width:30, fixed: true},
{field:'domain', title: '域名', width:110},
{field:'visit_date', title: '访问日期', width:120},
{field:'country', title: '国家', width:120},
{field:'result', title: '是否正常', width:80},
{field:'reason', title: '理由', width:120},
{field:'judge_timing_text', title: '判断用时', width:260},
{field:'vtimes', title: '第几次访问', width:90},
{field:'referer', title: '来源', width:210},
{field:'IP', title: 'IP(支持IPV6)', width:120},
{field:'client', title: '客户端', width:80},
{field:'language', title: '语言', width:80},
{field:'browser', title: '浏览器', width:80},
{field:'page', title: '访问页面', width:210},
{field:'campagin_id', title: '广告策略', width:80},
{field:'user_agent', title: 'User-Agent', width:320, hide: !showMore},
{field:'http_referer', title: 'HTTP-Referer', width:260, hide: !showMore},
{field:'accept_language_raw', title: 'Accept-Language', width:220, hide: !showMore},
]];
}
table.render({
elem: '#ID-table-demo-search',
url: '../datatable/sdatalist.php',
cols: buildCols(false),
page: true,
limit: 50,
height: 800,
size: "sm",
skin: "grid", // grid|line|row|nob
rowStyle: function(row) {
if (row.reason && row.reason.indexOf('未确定流量来源') !== -1) {
return 'color:#856404;background-color:#fff3cd;';
}
},
done: function(res, curr, count, origin){
console.log(res); // 得到当前渲染的数据
console.log(curr); // 得到当前页码
console.log(count); // 得到数据总量
console.log(origin); // 回调函数所执行的来源 --- 2.8.7+
$("#total_nums").text(count);
},
});
$('#btn-show-more').on('click', function() {
showMoreCols = !showMoreCols;
$(this).text(showMoreCols ? '收起更多' : '显示更多');
table.reload('ID-table-demo-search', { cols: buildCols(showMoreCols) });
});
// var laypage = layui.laypage;
// // 完整显示
// laypage.render({
// elem: 'demo-laypage-all', // 元素 id
// count: 10000, // 数据总数
// layout: ['count', 'prev', 'page', 'next', 'limit', 'refresh', 'skip'], // 功能布局
// jump: function(obj){
// console.log(obj);
// }
// });
// 日期
laydate.render({
elem: '.demo-table-search-date'
});
// 搜索提交
form.on('submit(demo-table-search)', function(data){
var field = data.field; // 获得表单字段
// 执行搜索重载
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
where: field // 搜索的字段
});
// layer.msg('搜索成功<br>此处为静态模拟数据,实际使用时换成真实接口即可');
return false; // 阻止默认 form 跳转
});
// table.reload('ID-table-demo-search', {
// page: {
// curr: 1 // 重新从第 1 页开始
// },
// });
var layer = layui.layer;
var util = layui.util;
util.on('lay-on', {
confirm: function(){
layer.confirm('确定清除所有日志数据?', {
btn: ['确定', '关闭'] //按钮
}, function(){
}, function(){
});
},
});
});
// 刷新按钮
function resetTable() {
var table = layui.table;
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
});
}
</script>
</body>
</html>
View File
Executable
+477
View File
@@ -0,0 +1,477 @@
<?php
require_once("cong.php");
session_start();
if(empty($_SESSION['password']) || $_SESSION['password'] != LOGIN_PASSWORD) {
unset($_SESSION['password']);
unset($_SESSION['check_result']);
echo("<script language=javascript>alert('密码错误!');window.location='ip_check_edit.php'; </script>");
//die("输入的密码不正确,请重新输入。");
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>访问判断日志</title>
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="//cdn.staticfile.org/layui/2.9.2/css/layui.css" rel="stylesheet">
<style>
:root {
--log-primary: #16baaa;
--log-primary-dark: #0e8d81;
--log-bg: #f0f2f5;
--log-card: #ffffff;
--log-border: #e8ecf1;
--log-header-bg: #f4f7fb;
--log-text: #2c3e50;
--log-muted: #8c98a4;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--log-bg);
color: var(--log-text);
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
}
.log-page {
max-width: 1680px;
margin: 0 auto;
padding: 24px 28px 40px;
}
.log-page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
flex-wrap: wrap;
gap: 12px;
}
.log-page-header h1 {
margin: 0;
font-size: 1.35rem;
font-weight: 600;
color: var(--log-text);
}
.log-page-header p {
margin: 4px 0 0;
font-size: .85rem;
color: var(--log-muted);
}
.log-card {
background: var(--log-card);
border: 1px solid var(--log-border);
border-radius: 10px;
box-shadow: 0 2px 12px rgba(15, 23, 42, .04);
margin-bottom: 16px;
overflow: hidden;
}
.log-card-body { padding: 18px 20px; }
.log-filter-grid .layui-col-md3 { margin-bottom: 12px; }
.log-filter-grid .layui-input,
.log-filter-grid .layui-select-title input {
border-radius: 6px;
border-color: var(--log-border);
}
.log-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 14px 20px;
border-top: 1px solid var(--log-border);
background: #fafbfc;
}
.log-actions .layui-btn { border-radius: 6px; }
.log-actions .layui-btn-primary { background: #fff; }
.log-count {
margin-left: auto;
font-size: .88rem;
color: var(--log-muted);
}
.log-count strong {
color: var(--log-primary-dark);
font-size: 1rem;
margin: 0 4px;
}
#btn-clear { background: #e74c3c !important; border-color: #e74c3c !important; }
.log-table-wrap { padding: 0; }
.log-table-wrap .layui-table-view {
border: none;
border-radius: 0 0 10px 10px;
margin: 0;
}
/* Sticky header */
.log-table-wrap .layui-table-header {
position: sticky;
top: 0;
z-index: 20;
border-radius: 0;
}
.log-table-wrap .layui-table-header .layui-table {
background: linear-gradient(180deg, #eef3f9 0%, #e4ebf5 100%);
}
.log-table-wrap .layui-table-header th {
color: #3d5167 !important;
font-weight: 600 !important;
font-size: 12.5px;
border-color: #d8e0ea !important;
padding: 11px 10px !important;
}
/* Zebra + hover */
.log-table-wrap .layui-table-body tr:nth-child(even) td {
background-color: #fafbfc;
}
.log-table-wrap .layui-table-body tr:hover td {
background-color: #eef6ff !important;
}
.log-table-wrap .layui-table-body td {
border-color: #eef1f5 !important;
padding: 9px 10px !important;
font-size: 12.5px;
line-height: 1.45;
transition: background-color .15s ease;
}
.log-table-wrap .layui-table-page {
border-top: 1px solid var(--log-border);
padding: 12px 16px;
background: #fafbfc;
}
/* Badges */
.cloak-badge {
display: inline-block;
padding: 2px 9px;
border-radius: 999px;
font-size: 11.5px;
font-weight: 600;
line-height: 1.5;
white-space: nowrap;
letter-spacing: .02em;
}
.badge-success { background: #d4edda; color: #155724; }
.badge-error { background: #f8d7da; color: #721c24; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-info { background: #d1ecf1; color: #0c5460; }
.badge-muted { background: #e9ecef; color: #495057; }
.reason-badge {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
vertical-align: middle;
}
/* Truncation + mono timestamp */
.cell-ellipsis {
display: block;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
cursor: default;
}
.cell-mono {
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
font-size: 12px;
color: #4a5568;
letter-spacing: -.02em;
}
.cell-ip { font-family: 'SF Mono', 'Consolas', monospace; font-size: 12px; }
.text-muted { color: #adb5bd; }
@media (max-width: 768px) {
.log-page { padding: 16px 12px 32px; }
.log-count { margin-left: 0; width: 100%; }
}
</style>
</head>
<body>
<div class="log-page">
<div class="log-page-header">
<div>
<h1>访问判断日志</h1>
<p>访客判定记录 · 支持筛选、导出与分页浏览</p>
</div>
<div class="log-nav" style="display:flex;gap:8px;">
<a href="table.php" style="display:inline-block;padding:6px 14px;border-radius:6px;font-size:.85rem;text-decoration:none;border:1px solid #e8ecf1;background:#16baaa;color:#fff;">访问日志</a>
<a href="log_analytics.php" style="display:inline-block;padding:6px 14px;border-radius:6px;font-size:.85rem;text-decoration:none;border:1px solid #e8ecf1;background:#fff;color:#2c3e50;">数据分析</a>
</div>
</div>
<div class="log-card">
<div class="log-card-body">
<form class="layui-form layui-row layui-col-space16 log-filter-grid">
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-username"></i>
</div>
<input type="text" name="domain" value="" placeholder="域名" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="spage" value="" placeholder="访问页面,可输入配置名称搜索" class="layui-input" lay-affix="clear">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<select name="result">
<option value="">全部(不含检测中)</option>
<option value="true">TRUE(通过访问)</option>
<option value="false">FALSE(禁止访问)</option>
<option value="wait">检测中</option>
</select>
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="country" placeholder="国家" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<input type="text" name="IP" placeholder="IP" lay-affix="clear" class="layui-input">
</div>
</div>
<div class="layui-col-md3">
<div class="layui-input-wrap">
<div class="layui-input-prefix">
<i class="layui-icon layui-icon-date"></i>
</div>
<input type="text" name="visit_date" readonly placeholder="访问日期" class="layui-input demo-table-search-date">
</div>
</div>
<div class="layui-btn-container layui-col-xs12">
<a class="layui-btn" lay-submit href="javascript:void(0);" id="export" target="_blank">导出</a>
<button class="layui-btn" lay-submit lay-filter="demo-table-search">搜索</button>
<button type="reset" class="layui-btn layui-btn-primary">清除条件</button>
</div>
</form>
</div>
<div class="log-actions">
<button class="layui-btn layui-btn-sm" id="btn-refresh" onclick="resetTable()" lay-event="footerDemoBtn1">刷新</button>
<button class="layui-btn layui-btn-sm layui-btn-normal" id="btn-show-more" type="button">显示更多</button>
<button class="layui-btn layui-btn-sm btn-danger" id="btn-clear" lay-on="confirm" lay-event="footerDemoBtn2">清空数据库</button>
<span class="log-count">当前查询数量:<strong id="total_nums">—</strong></span>
</div>
</div>
<div class="log-card log-table-wrap">
<div id="demo-laypage-all"></div>
<table class="layui-hide" id="ID-table-demo-search" lay-filter="test"></table>
</div>
</div>
<script src="/dist/layui.js"></script>
<script src="/dist/jquery.min.js"></script>
<script>
layui.use(function(){
var table = layui.table;
var form = layui.form;
var laydate = layui.laydate;
var showMoreCols = false;
function escapeHtml(s) {
if (s == null || s === '') return '';
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function cellEllipsis(value, maxLen) {
var text = value == null ? '' : String(value);
if (!text) return '<span class="text-muted">—</span>';
var esc = escapeHtml(text);
var display = text.length > maxLen ? escapeHtml(text.slice(0, maxLen)) + '…' : esc;
return '<span class="cell-ellipsis" title="' + esc + '">' + display + '</span>';
}
function badgeResult(val) {
var raw = val == null ? '' : String(val);
var v = raw.toLowerCase();
if (v === 'true' || v === '1') {
return '<span class="cloak-badge badge-success">TRUE</span>';
}
if (v === 'false' || v === '0') {
return '<span class="cloak-badge badge-error">FALSE</span>';
}
if (v === 'wait' || raw.indexOf('检测') !== -1) {
return '<span class="cloak-badge badge-info">检测中</span>';
}
return '<span class="cloak-badge badge-muted">' + escapeHtml(raw) + '</span>';
}
function badgeReason(reason) {
if (reason == null || reason === '') {
return '<span class="text-muted">—</span>';
}
var text = String(reason);
var cls = 'badge-info';
if (text.indexOf('未确定流量来源') !== -1) {
cls = 'badge-warning';
} else if (/过滤|黑名单|无效|代理|VPN|禁止|欺骗|无来源/i.test(text)) {
cls = 'badge-error';
} else if (/白名单|允许|通过/i.test(text)) {
cls = 'badge-success';
}
var esc = escapeHtml(text);
var short = text.length > 26 ? escapeHtml(text.slice(0, 26)) + '…' : esc;
return '<span class="cloak-badge ' + cls + ' reason-badge" title="' + esc + '">' + short + '</span>';
}
function buildCols(showMore) {
return [[
{field:'id', title: 'ID', width:56, fixed: true},
{field:'domain', title: '域名', width:120, templet: function(d){ return cellEllipsis(d.domain, 18); }},
{field:'visit_date', title: '访问日期', width:130, templet: function(d){
return '<span class="cell-mono" title="' + escapeHtml(d.visit_date) + '">' + escapeHtml(d.visit_date) + '</span>';
}},
{field:'country', title: '国家', width:100, templet: function(d){ return cellEllipsis(d.country, 12); }},
{field:'result', title: '是否正常', width:88, templet: function(d){ return badgeResult(d.result); }},
{field:'reason', title: '理由', width:140, templet: function(d){ return badgeReason(d.reason); }},
{field:'judge_timing_text', title: '判断用时', width:240, templet: function(d){ return cellEllipsis(d.judge_timing_text, 36); }},
{field:'vtimes', title: '第几次访问', width:96},
{field:'fp_url', title: '跳转至', width:180, templet: function(d){ return cellEllipsis(d.fp_url, 28); }},
{field:'device', title: '访问设备', width:200, templet: function(d){ return cellEllipsis(d.device, 32); }},
{field:'referer', title: '来源', width:200, templet: function(d){ return cellEllipsis(d.referer, 32); }},
{field:'IP', title: 'IP(支持IPV6)', width:130, templet: function(d){
return '<span class="cell-ip" title="' + escapeHtml(d.IP) + '">' + escapeHtml(d.IP) + '</span>';
}},
{field:'client', title: '客户端', width:80, templet: function(d){ return cellEllipsis(d.client, 10); }},
{field:'language', title: '语言', width:80, templet: function(d){ return cellEllipsis(d.language, 10); }},
{field:'browser', title: '浏览器', width:88, templet: function(d){ return cellEllipsis(d.browser, 10); }},
{field:'page', title: '访问页面', width:200, templet: function(d){ return cellEllipsis(d.page, 32); }},
{field:'campagin_id', title: '广告策略', width:88, templet: function(d){ return cellEllipsis(d.campagin_id, 10); }},
{field:'user_agent', title: 'User-Agent', width:320, hide: !showMore, templet: function(d){ return cellEllipsis(d.user_agent, 48); }},
{field:'http_referer', title: 'HTTP-Referer', width:260, hide: !showMore, templet: function(d){ return cellEllipsis(d.http_referer, 40); }},
{field:'accept_language_raw', title: 'Accept-Language', width:220, hide: !showMore, templet: function(d){ return cellEllipsis(d.accept_language_raw, 32); }},
]];
}
table.render({
elem: '#ID-table-demo-search',
url: 'datatable/datalist.php',
cols: buildCols(false),
page: true,
limit: 50,
height: 800,
size: "sm",
skin: "line",
even: true,
rowStyle: function(row) {
if (row.reason && row.reason.indexOf('未确定流量来源') !== -1) {
return 'color:#856404;background-color:#fff8e6;';
}
},
done: function(res, curr, count, origin){
console.log(res); // 得到当前渲染的数据
console.log(curr); // 得到当前页码
console.log(count); // 得到数据总量
console.log(origin); // 回调函数所执行的来源 --- 2.8.7+
$("#total_nums").text(count);
},
});
$('#btn-show-more').on('click', function() {
showMoreCols = !showMoreCols;
$(this).text(showMoreCols ? '收起更多' : '显示更多');
table.reload('ID-table-demo-search', { cols: buildCols(showMoreCols) });
});
// 日期
laydate.render({
elem: '.demo-table-search-date'
});
// 搜索提交
form.on('submit(demo-table-search)', function(data){
var field = data.field; // 获得表单字段
// 执行搜索重载
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
where: field // 搜索的字段
});
// layer.msg('搜索成功<br>此处为静态模拟数据,实际使用时换成真实接口即可');
return false; // 阻止默认 form 跳转
});
// table.reload('ID-table-demo-search', {
// page: {
// curr: 1 // 重新从第 1 页开始
// },
// });
var layer = layui.layer;
var util = layui.util;
util.on('lay-on', {
confirm: function(){
layer.confirm('确定清除所有日志数据?', {
btn: ['确定', '关闭'] //按钮
}, function(){
$.get("datatable/datalist.php", {clear: "true"}, function(result){
layer.msg('清除成功', {icon: 1});
window.location.reload();
});
}, function(){
});
},
});
});
// 刷新按钮
function resetTable() {
var table = layui.table;
table.reload('ID-table-demo-search', {
page: {
curr: 1 // 重新从第 1 页开始
},
});
}
$("#export").click(function() {
var visit_date = $("input[name=visit_date]").val();
var domain = $("input[name=domain]").val();
var spage = $("input[name=spage]").val();
var country = $("input[name=country]").val();
var IP = $("input[name=IP]").val();
var result = $("select[name=result]").val();
var url = 'datatable/export.php?';
var args = [];
if(visit_date) {
args.push( "visit_date=" + visit_date );
}
if(domain) {
args.push( "domain=" + domain );
}
if(spage) {
args.push( "spage=" + spage );
}
if(country) {
args.push( "country=" + country );
}
if(IP) {
args.push( "IP=" + IP );
}
if(result) {
args.push( "result=" + encodeURIComponent(result) );
}
window.location.href=url + args.join("&");
});
</script>
</body>
</html>
Executable
+4
View File
@@ -0,0 +1,4 @@
<?php
require('ip_check.php');

Some files were not shown because too many files have changed in this diff Show More