Вход Регистрация
Файл: new_top/core/classes/AntiCheat.php
Строк: 254
<?php
/**
 * Anti-cheat for counter hits and inbound clicks
 * Self-contained — no external services
 */

declare(strict_types=1);

class 
AntiCheat
{
    
/** Min seconds between counted hits from same IP+platform */
    
public const HIT_INTERVAL 4;

    
/** Max hits per IP per platform per calendar day */
    
public const MAX_HITS_IP_PF_DAY 120;

    
/** Max hits per IP across all platforms per hour */
    
public const MAX_HITS_IP_HOUR 200;

    
/** Max unique platforms one IP may inflate per hour */
    
public const MAX_PF_IP_HOUR 25;

    
/** Min seconds between inbound clicks same IP+platform */
    
public const IN_INTERVAL 8;

    
/** Max inbound clicks per IP per platform per day */
    
public const MAX_IN_IP_PF_DAY 30;

    
/** Hits/hosts ratio above this is suspicious (daily) */
    
public const SUSPECT_HT_HS_RATIO 40;

    
/**
     * Decide whether this view should be counted.
     * Always returns platform row handling is outside — here only allow/deny + reason.
     *
     * @return array{allow:bool, reason:string}
     */
    
public static function checkHit(int $platformIdstring $ipstring $ua): array
    {
        if (
self::isIpBanned($ip)) {
            
self::log($platformId$ip$ua'hit''ip_banned');
            return [
'allow' => false'reason' => 'ip_banned'];
        }

        if (
self::isBadUa($ua)) {
            
self::log($platformId$ip$ua'hit''bad_ua');
            return [
'allow' => false'reason' => 'bad_ua'];
        }

        if (
self::isPrivateOrInvalidIp($ip)) {
            
self::log($platformId$ip$ua'hit''bad_ip');
            return [
'allow' => false'reason' => 'bad_ip'];
        }

        
$now time();
        
$dayStart mktime(000, (int)date('m'), (int)date('d'), (int)date('Y'));
        
$hourAgo $now 3600;

        
// Interval: last hit from this IP on this platform
        
$last Database::fetchColumn(
            
'SELECT time FROM ht WHERE pf = ? AND ip = ? ORDER BY id DESC LIMIT 1',
            [
$platformId$ip]
        );
        if (
$last && ($now - (int)$last) < self::HIT_INTERVAL) {
            
self::log($platformId$ip$ua'hit''too_fast');
            return [
'allow' => false'reason' => 'too_fast'];
        }

        
// Daily cap per IP+platform
        
$dayHits = (int) Database::fetchColumn(
            
'SELECT COUNT(*) FROM ht WHERE pf = ? AND ip = ? AND time >= ?',
            [
$platformId$ip$dayStart]
        );
        if (
$dayHits >= self::MAX_HITS_IP_PF_DAY) {
            
self::log($platformId$ip$ua'hit''day_cap');
            return [
'allow' => false'reason' => 'day_cap'];
        }

        
// Global hourly cap per IP
        
$hourHits = (int) Database::fetchColumn(
            
'SELECT COUNT(*) FROM ht WHERE ip = ? AND time >= ?',
            [
$ip$hourAgo]
        );
        if (
$hourHits >= self::MAX_HITS_IP_HOUR) {
            
self::log($platformId$ip$ua'hit''hour_cap');
            return [
'allow' => false'reason' => 'hour_cap'];
        }

        
// Too many different platforms from one IP in an hour
        
$pfHour = (int) Database::fetchColumn(
            
'SELECT COUNT(DISTINCT pf) FROM ht WHERE ip = ? AND time >= ?',
            [
$ip$hourAgo]
        );
        if (
$pfHour >= self::MAX_PF_IP_HOUR) {
            
self::log($platformId$ip$ua'hit''multi_pf');
            return [
'allow' => false'reason' => 'multi_pf'];
        }

        return [
'allow' => true'reason' => 'ok'];
    }

    
/**
     * @return array{allow:bool, reason:string}
     */
    
public static function checkIn(int $platformIdstring $ipstring $ua): array
    {
        if (
self::isIpBanned($ip)) {
            return [
'allow' => false'reason' => 'ip_banned'];
        }
        if (
self::isBadUa($ua)) {
            return [
'allow' => false'reason' => 'bad_ua'];
        }

        
$now time();
        
$dayStart mktime(000, (int)date('m'), (int)date('d'), (int)date('Y'));

        
$last Database::fetchColumn(
            
'SELECT time FROM in_log WHERE pf = ? AND ip = ? ORDER BY id DESC LIMIT 1',
            [
$platformId$ip]
        );
        if (
$last && ($now - (int)$last) < self::IN_INTERVAL) {
            
self::log($platformId$ip$ua'in''too_fast');
            return [
'allow' => false'reason' => 'too_fast'];
        }

        
$dayIn = (int) Database::fetchColumn(
            
'SELECT COUNT(*) FROM in_log WHERE pf = ? AND ip = ? AND time >= ?',
            [
$platformId$ip$dayStart]
        );
        if (
$dayIn >= self::MAX_IN_IP_PF_DAY) {
            
self::log($platformId$ip$ua'in''day_cap');
            return [
'allow' => false'reason' => 'day_cap'];
        }

        return [
'allow' => true'reason' => 'ok'];
    }

    public static function 
isBadUa(string $ua): bool
    
{
        
$ua trim($ua);
        if (
$ua === '' || mb_strlen($ua) < 8) {
            return 
true;
        }
        
$low mb_strtolower($ua);
        
$bots = [
            
'bot''spider''crawl''slurp''wget''curl''python-requests',
            
'scrapy''httpclient''libwww''java/''php/''go-http',
            
'headless''selenium''puppeteer''phantom''monitor',
            
'uptime''pingdom''gtmetrix''preview',
        ];
        foreach (
$bots as $b) {
            if (
strpos($low$b) !== false) {
                return 
true;
            }
        }
        return 
false;
    }

    public static function 
isPrivateOrInvalidIp(string $ip): bool
    
{
        if (
$ip === '' || $ip === '0.0.0.0' || $ip === '127.0.0.1' || $ip === '::1') {
            return 
true;
        }
        if (
filter_var($ipFILTER_VALIDATE_IPFILTER_FLAG_NO_PRIV_RANGE FILTER_FLAG_NO_RES_RANGE) === false) {
            
// private/reserved — still allow in local/dev if explicitly public check fails
            // For tops on public web, private IPs are often proxies misconfig — soft: allow counting but mark
            // Strict mode: treat as invalid only when not valid IP at all
            
if (filter_var($ipFILTER_VALIDATE_IP) === false) {
                return 
true;
            }
        }
        return 
false;
    }

    public static function 
isIpBanned(string $ip): bool
    
{
        try {
            
$row Database::fetch(
                
'SELECT ip FROM ip_ban WHERE ip = ? AND (until_time = 0 OR until_time > ?) LIMIT 1',
                [
$iptime()]
            );
            return (bool)
$row;
        } catch (
Throwable $e) {
            return 
false;
        }
    }

    public static function 
banIp(string $ipstring $reason ''int $hours 0int $adminId 0): void
    
{
        
$until $hours time() + $hours 3600 0;
        
Database::query(
            
'INSERT INTO ip_ban (ip, reason, until_time, time_add, admin_id) VALUES (?, ?, ?, ?, ?)
             ON DUPLICATE KEY UPDATE reason = VALUES(reason), until_time = VALUES(until_time), admin_id = VALUES(admin_id)'
,
            [
mb_substr($ip045), mb_substr($reason0200), $untiltime(), $adminId]
        );
    }

    public static function 
unbanIp(string $ip): void
    
{
        
Database::query('DELETE FROM ip_ban WHERE ip = ?', [$ip]);
    }

    public static function 
log(int $pfstring $ipstring $uastring $kindstring $reason): void
    
{
        try {
            
Database::query(
                
'INSERT INTO cheat_log (pf, ip, ua, kind, reason, time) VALUES (?, ?, ?, ?, ?, ?)',
                [
$pfmb_substr($ip045), mb_substr($ua0255), $kind$reasontime()]
            );
        } catch (
Throwable $e) {
            
// table may not exist yet on old installs
        
}
    }

    
/**
     * Flag platforms with abnormal ht/hs ratio today
     * @return list<array>
     */
    
public static function suspiciousPlatforms(int $limit 20): array
    {
        return 
Database::fetchAll(
            
'SELECT id, url, hs, ht, user_id,
                    CASE WHEN hs > 0 THEN ROUND(ht / hs, 1) ELSE ht END AS ratio
             FROM platforms
             WHERE mode = 0 AND ban = 0 AND ht > 50
               AND (hs = 0 OR (ht / GREATEST(hs, 1)) >= ?)
             ORDER BY ratio DESC
             LIMIT ' 
. (int)$limit,
            [
self::SUSPECT_HT_HS_RATIO]
        );
    }

    
/**
     * Top IPs by hit volume today (for admin)
     */
    
public static function topIpsToday(int $limit 30): array
    {
        
$dayStart mktime(000, (int)date('m'), (int)date('d'), (int)date('Y'));
        return 
Database::fetchAll(
            
'SELECT ip, COUNT(*) AS hits, COUNT(DISTINCT pf) AS platforms
             FROM ht WHERE time >= ?
             GROUP BY ip
             ORDER BY hits DESC
             LIMIT ' 
. (int)$limit,
            [
$dayStart]
        );
    }

    public static function 
recentBlocks(int $limit 50): array
    {
        try {
            return 
Database::fetchAll(
                
'SELECT * FROM cheat_log ORDER BY id DESC LIMIT ' . (int)$limit
            
);
        } catch (
Throwable $e) {
            return [];
        }
    }

    public static function 
reasonLabel(string $code): string
    
{
        switch (
$code) {
            case 
'ip_banned': return 'IP в бане';
            case 
'bad_ua': return 'Бот / пустой UA';
            case 
'bad_ip': return 'Некорректный IP';
            case 
'too_fast': return 'Слишком часто';
            case 
'day_cap': return 'Дневной лимит';
            case 
'hour_cap': return 'Часовой лимит';
            case 
'multi_pf': return 'Много площадок с IP';
            default: return 
$code;
        }
    }
}
Онлайн: 1
Реклама