新增maxmind数据库本地判断国家,并且将优先级调整至最高

This commit is contained in:
root
2026-06-14 15:29:29 +08:00
parent 72d388f642
commit eab59c1c18
17 changed files with 912 additions and 38 deletions
+89
View File
@@ -0,0 +1,89 @@
<?php
/**
* CDN / 反代场景下解析访客真实 IP
*/
class ClientIpResolver
{
/**
* @return string
*/
public static function resolve()
{
static $resolved = null;
if ($resolved !== null) {
return $resolved;
}
$candidates = [];
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$candidates[] = (string) $_SERVER['HTTP_CF_CONNECTING_IP'];
}
if (!empty($_SERVER['HTTP_TRUE_CLIENT_IP'])) {
$candidates[] = (string) $_SERVER['HTTP_TRUE_CLIENT_IP'];
}
if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$candidates[] = (string) $_SERVER['HTTP_X_REAL_IP'];
}
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
foreach (explode(',', (string) $_SERVER['HTTP_X_FORWARDED_FOR']) as $part) {
$part = trim($part);
if ($part !== '' && strcasecmp($part, 'unknown') !== 0) {
$candidates[] = $part;
}
}
}
if (!empty($_SERVER['REMOTE_ADDR'])) {
$candidates[] = (string) $_SERVER['REMOTE_ADDR'];
}
foreach ($candidates as $candidate) {
$ip = self::normalizeIp($candidate);
if ($ip === null) {
continue;
}
if (self::isPublicIp($ip)) {
$resolved = $ip;
return $resolved;
}
}
foreach ($candidates as $candidate) {
$ip = self::normalizeIp($candidate);
if ($ip !== null) {
$resolved = $ip;
return $resolved;
}
}
$resolved = '0.0.0.0';
return $resolved;
}
/**
* @param string $ip
*/
public static function normalizeIp($ip)
{
$ip = trim($ip);
if ($ip === '' || strcasecmp($ip, 'unknown') === 0) {
return null;
}
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6) === false) {
return null;
}
return $ip;
}
/**
* @param string $ip
*/
private static function isPublicIp($ip)
{
return filter_var(
$ip,
FILTER_VALIDATE_IP,
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE
) !== false;
}
}
+42
View File
@@ -0,0 +1,42 @@
<?php
/**
* SHOW_SITE_COUNTRY 允许列表解析与比对
*/
class CountryAllowlist
{
/**
* @param string $config 逗号分隔国家码,如 US,GB,CA
* @return string[]
*/
public static function parse($config)
{
$config = strtoupper(trim((string) $config));
if ($config === '') {
return [];
}
$parts = preg_split('/\s*,\s*/', $config);
$out = [];
foreach ($parts as $part) {
$part = trim($part);
if ($part !== '') {
$out[] = $part;
}
}
return array_values(array_unique($out));
}
/**
* @param string|null $countryCode
* @param string[] $allowlist
*/
public static function isAllowed($countryCode, array $allowlist)
{
if ($allowlist === []) {
return true;
}
if ($countryCode === null || $countryCode === '') {
return false;
}
return in_array(strtoupper($countryCode), $allowlist, true);
}
}
+168
View File
@@ -0,0 +1,168 @@
<?php
/**
* IP → 国家码跨请求缓存(Redis 优先,文件降级,默认 TTL 30 天)
*/
class GeoIpCountryCache
{
const REDIS_PREFIX = 'cloak:geoip:cc:';
const NONE_SENTINEL = '__NONE__';
/** @var bool|null */
private static $redisAvailable = null;
/**
* @param string $ip
* @return string|false|null 命中返回国家码或 null(已知无国家);false=未命中
*/
public static function get($ip)
{
$ip = ClientIpResolver::normalizeIp($ip);
if ($ip === null) {
return false;
}
$redis = self::redisClient();
if ($redis) {
try {
$value = $redis->get(self::REDIS_PREFIX . $ip);
if ($value === false || $value === null) {
return false;
}
return $value === self::NONE_SENTINEL ? null : (string) $value;
} catch (Throwable $e) {
self::logError('Redis get failed: ' . $e->getMessage());
}
}
return self::getFile($ip);
}
/**
* @param string $ip
* @param string|null $countryCode
*/
public static function set($ip, $countryCode)
{
$ip = ClientIpResolver::normalizeIp($ip);
if ($ip === null) {
return;
}
$value = $countryCode !== null && $countryCode !== '' ? strtoupper($countryCode) : self::NONE_SENTINEL;
$ttl = self::cacheTtl();
$redis = self::redisClient();
if ($redis) {
try {
$redis->setex(self::REDIS_PREFIX . $ip, $ttl, $value);
return;
} catch (Throwable $e) {
self::logError('Redis set failed: ' . $e->getMessage());
}
}
self::setFile($ip, $value, $ttl);
}
private static function cacheTtl()
{
$ttl = defined('CLOAK_GEOIP_CACHE_TTL') ? (int) CLOAK_GEOIP_CACHE_TTL : 2592000;
return max(60, $ttl);
}
/**
* @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 cacheDir()
{
$dir = dirname(__DIR__, 2) . '/storage/runtime/c7f2a9e1/cc_cache';
if (!is_dir($dir)) {
@mkdir($dir, 0755, true);
}
return $dir;
}
private static function cacheFilePath($ip)
{
return self::cacheDir() . '/' . hash('sha256', $ip) . '.json';
}
/**
* @param string $ip
* @return string|false|null
*/
private static function getFile($ip)
{
$path = self::cacheFilePath($ip);
if (!is_file($path)) {
return false;
}
$raw = @file_get_contents($path);
if ($raw === false || $raw === '') {
return false;
}
$data = json_decode($raw, true);
if (!is_array($data) || empty($data['expires_at']) || empty($data['value'])) {
@unlink($path);
return false;
}
if ((int) $data['expires_at'] < time()) {
@unlink($path);
return false;
}
return $data['value'] === self::NONE_SENTINEL ? null : (string) $data['value'];
}
/**
* @param string $ip
* @param string $value
* @param int $ttl
*/
private static function setFile($ip, $value, $ttl)
{
$payload = json_encode([
'value' => $value,
'expires_at' => time() + $ttl,
], JSON_UNESCAPED_UNICODE);
if ($payload === false) {
return;
}
@file_put_contents(self::cacheFilePath($ip), $payload, LOCK_EX);
}
public static function logError($message)
{
$handle = @fopen(dirname(__DIR__, 2) . '/err.txt', 'a+');
if ($handle) {
fwrite($handle, date('Y-m-d H:i:s') . ' | GeoIpCountryCache | ' . $message . "\n");
fclose($handle);
}
}
}
+111
View File
@@ -0,0 +1,111 @@
<?php
class GeoIpCountryResolver
{
/** @var GeoIp2\Database\Reader|null */
private static $reader = null;
/** @var array<string, string|null> */
private static $requestCache = [];
/** @var string|null */
private static $lastSource = null;
/**
* @param string $ip
* @return array{code:?string, source:string}
*/
public static function resolveWithMeta($ip)
{
$ip = ClientIpResolver::normalizeIp($ip);
if ($ip === null) {
self::$lastSource = 'invalid_ip';
return ['code' => null, 'source' => 'invalid_ip'];
}
if (array_key_exists($ip, self::$requestCache)) {
self::$lastSource = 'request';
return ['code' => self::$requestCache[$ip], 'source' => 'request'];
}
$cached = GeoIpCountryCache::get($ip);
if ($cached !== false) {
self::$requestCache[$ip] = $cached;
self::$lastSource = 'cache';
return ['code' => $cached, 'source' => 'cache'];
}
$code = self::lookupMmdb($ip);
GeoIpCountryCache::set($ip, $code);
self::$requestCache[$ip] = $code;
self::$lastSource = 'mmdb';
return ['code' => $code, 'source' => 'mmdb'];
}
/**
* @param string $ip
* @return string|null ISO 3166-1 alpha-2
*/
public static function resolve($ip)
{
$result = self::resolveWithMeta($ip);
return $result['code'];
}
public static function lastSource()
{
return self::$lastSource ?? '';
}
public static function isAvailable()
{
return self::getReader() !== null;
}
/**
* @param string $ip
* @return string|null
*/
private static function lookupMmdb($ip)
{
$reader = self::getReader();
if ($reader === null) {
return null;
}
try {
$record = $reader->country($ip);
$code = $record->country->isoCode ?? null;
if ($code === null || $code === '') {
return null;
}
return strtoupper((string) $code);
} catch (GeoIp2\Exception\AddressNotFoundException $e) {
return null;
} catch (Throwable $e) {
GeoIpCountryCache::logError('mmdb lookup failed: ' . $e->getMessage());
return null;
}
}
/**
* @return GeoIp2\Database\Reader|null
*/
private static function getReader()
{
if (self::$reader !== null) {
return self::$reader;
}
if (!class_exists('GeoIp2\\Database\\Reader')) {
return null;
}
if (!defined('CLOAK_GEOIP_DB_PATH') || !is_file(CLOAK_GEOIP_DB_PATH)) {
return null;
}
try {
self::$reader = new GeoIp2\Database\Reader(CLOAK_GEOIP_DB_PATH);
return self::$reader;
} catch (Throwable $e) {
GeoIpCountryCache::logError('Reader init failed: ' . $e->getMessage());
return null;
}
}
}
+8 -1
View File
@@ -3,7 +3,7 @@
* 流量判定主流水线
*
* 阶段顺序(不可调换):
* 1. Session 快速路径 → 2. 黑白名单 → 3. 广告来源限制4. 临时链接 → 5. API/指纹
* 1. Session 快速路径 → 2. GeoIP 国家 → 3. 黑白名单 → 4. 广告来源 → 5. 临时链接 → 6. API/指纹
*/
class CheckPipeline
{
@@ -34,6 +34,13 @@ class CheckPipeline
cloak_dbg_step(__LINE__, 'Session 快速路径', 'skip', '未命中缓存,进入完整判定流程(DEBUG 已重置 visit_to_*');
}
CloakPipelineTimer::stage('GeoIP国家判定');
include __DIR__ . '/stages/check_country_geoip.inc.php';
if (!empty($_SESSION['check_result'])) {
CloakPipelineTimer::finish();
return;
}
CloakPipelineTimer::stage('黑白名单');
include __DIR__ . '/stages/apply_whitelist_blacklist.inc.php';
CloakPipelineTimer::stage('广告来源限制');
@@ -0,0 +1,59 @@
<?php
/**
* 屏蔽模式:GeoIP 国家判定(本地 MaxMind + 30 天 IP 缓存)
*
* 依赖全局:$v_info, $reason, $__cloak_debug_on
*/
if (!defined('SHOW_SITE_MODE_SWITCH') || SHOW_SITE_MODE_SWITCH !== 'ip_check') {
return;
}
if (!defined('CLOAK_GEOIP_ENABLED') || strtoupper(CLOAK_GEOIP_ENABLED) !== 'ON') {
return;
}
if (!empty($_SESSION['check_result'])) {
return;
}
$ip = ClientIpResolver::resolve();
if ($ip !== '' && $ip !== '0.0.0.0') {
$v_info->v_ip = $ip;
$_SESSION['gcu_ip'] = $ip;
}
$geoMeta = GeoIpCountryResolver::resolveWithMeta($ip);
$countryCode = $geoMeta['code'];
$geoSource = $geoMeta['source'];
if ($countryCode !== null && $countryCode !== '') {
$v_info->v_country = $countryCode;
$_SESSION['gcu_country'] = $countryCode;
}
$allowlist = CountryAllowlist::parse(defined('SHOW_SITE_COUNTRY') ? SHOW_SITE_COUNTRY : '');
if (!CountryAllowlist::isAllowed($countryCode, $allowlist)) {
$_SESSION['check_result'] = 'false';
if ($countryCode === null || $countryCode === '') {
$reason = '无法识别访客国家';
} else {
$reason = '国家不符:' . $countryCode;
}
if (!empty($GLOBALS['__cloak_debug_on'])) {
cloak_dbg_step(__LINE__, 'GeoIP国家判定', 'fail', $reason, [
'ip' => $ip,
'country' => $countryCode,
'allowlist' => $allowlist,
'geo_source' => $geoSource,
]);
cloak_dbg_record(__LINE__, 'false', $reason, ['stage' => 'geoip_country']);
}
return;
}
if (!empty($GLOBALS['__cloak_debug_on'])) {
cloak_dbg_step(__LINE__, 'GeoIP国家判定', 'pass', '国家允许或列表为空', [
'ip' => $ip,
'country' => $countryCode,
'allowlist' => $allowlist,
'geo_source' => $geoSource,
]);
}
+11 -35
View File
@@ -32,10 +32,9 @@ class visitorInfo
*/
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_country = !empty($_SESSION['gcu_country']) ? (string) $_SESSION['gcu_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();
@@ -45,8 +44,10 @@ class visitorInfo
$this->costm_ip_score = $costm_ip_score;
$this->check_key = $check_key;
$this->check_country = $check_country;
$_SESSION["gcu_country"] = $this->v_country;
if ($this->v_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;
@@ -71,7 +72,9 @@ class visitorInfo
$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;
if ($this->v_country === '' && !empty($return['country'])) {
$this->v_country = (string) $return['country'];
}
$this->v_reason = cloak_api_reason($return, $this->check_result);
return $return;
}
@@ -149,37 +152,10 @@ class visitorInfo
//获取访客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 ($type) {
return ClientIpResolver::resolve();
}
// 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;
return ClientIpResolver::resolve();
}
//客户当前浏览的页面 url(浏览器实际访问地址,含正确 scheme 与 query