feat: initial commit
This commit is contained in:
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
# 忽略 Cursor 和 VS Code 的编辑器缓存
|
||||||
|
.sidebar/
|
||||||
|
.cursor/
|
||||||
|
.vscode/
|
||||||
|
*.log
|
||||||
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
2026-05-24 21:55:43 installed
|
||||||
Executable
+21
@@ -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.
|
||||||
Executable
+233
File diff suppressed because one or more lines are too long
Executable
+321
@@ -0,0 +1,321 @@
|
|||||||
|

|
||||||
|
|
||||||
|
# MobileDetect, PHP mobile detection class
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

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