新增maxmind数据库本地判断国家,并且将优先级调整至最高
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user