Нижепредложенный скрипт был наконец написан (лет 6 назад), когда устал каждый день смотреть логи сервера, где чётко видно было "мусорные" запросы, раздувающие лог и дающие лишнюю нагрузку на хостинге.

Исключительно в познавательных целях! Нормальную защиту реализуют средствами сервера, а этот php-скрипт лишь превентивная мера.

Огромная часть бот-трафика это запросы вида:

Hidden text
  • 51.120.240.89 - - [01/Apr/2022:15:28:26 +0300] "GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1" 403 "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"

  • 51.120.240.89 - - [01/Apr/2022:15:28:44 +0300] "GET /wp-content/plugins/ubh/up.php/.well-known/ HTTP/1.1" 403 "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"

  • 51.120.240.89 - - [01/Apr/2022:15:29:16 +0300] "GET /wp-content/uploads/ HTTP/1.1" 403 "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/60.0.3112.107 Moblie Safari/537.36"

  • 51.120.240.89 - - [01/Apr/2022:15:29:42 +0300] "GET /wp-includes/ HTTP/1.1" 403 "Mozlila/5.0 (Linux; Android 7.0; SM-G892A Bulid/NRD90M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0

И все 50-200 строк в таком духе, за короткий промежуток времени.

То есть видим ip-адрес атакующего -- [дата:время] "вид запроса и собственно сам запрос (/wp-content/wp-includes)" код ответа сервера 403 (т.к. ip-адрес не российский, город Осло, но об этом в следующих постах) строка UserAgent (может быть любой).

Сам скрипт настраиваемый по частоте запросов в единицу времени. Например, 4 запроса за 1 секунду приведут к блокировке атакующего ip-адреса на 60 секунд.

Чтобы не было вопросов "А как же поисковые боты, типа Яндекса и пр." встроил проверку на поискового бота. Если это например робот Яндекса, то скрипт пропускает его и не проверяет больше ничего, не следит за активностью. Если это не из списка разрешённых ботов, то идёт отслеживание активности и если это откровенно "долбёжка", парсинг или как в вышеприведённом кусочке лога - попытка узнать/взломать вашу CMS - однозначно блокировка на указанное в настройках время (у меня 60 сек).

Собственно сам скрипт:

/*** Класс проверки и блокировки ip-адреса. */
class BotBlockIp {
    /*** Время блокировки в секундах. */
    const blockSeconds = 60;
    /**
     * Интервал времени запросов страниц.
     */
    const intervalSeconds = 1;
    /**
     * Количество запросов страницы в интервал времени.
     */
    const intervalTimes = 4;
    /**
     * Флаг подключения всегда активных пользователей.
     */
    const isAlwaysActive = true;
    /**
     * Флаг подключения всегда заблокированных пользователей.
     */
    const isAlwaysBlock = true;
    /**
     * Путь к директории кэширования активных пользователей.
     */
    const pathActive = 'active';
    /**
     * Путь к директории кэширования заблокированных пользователей.
     */
    const pathBlock = 'block';
    /**
     * Флаг абсолютных путей к директориям.
     */
    const pathIsAbsolute = false;
    /**
     * Список всегда активных пользователей.
     */
    public static $alwaysActive = array(
 
    );

    /**
     * Список всегда заблокированных пользователей.
     */
    public static $alwaysBlock = array(
 
    );

    /**
     * Метод проверки ip-адреса на активность и блокировку.
     */
    public static function checkIp() {
		
	// Если это поисковый бот, то выходим ничего не делая
	if(self::is_bot()){
		return;
	}

        // Получение ip-адреса
        $ip_address = self::_getIp();

        // Пропускаем всегда активных пользователей
        if (in_array($ip_address, self::$alwaysActive) && self::isAlwaysActive) {
            return;
        }

        // Блокируем всегда заблокированных пользователей
        if (in_array($ip_address, self::$alwaysBlock) && self::isAlwaysBlock) {
	    header('HTTP/1.0 403 Forbidden');
            echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
            echo '<html xmlns="http://www.w3.org/1999/xhtml">';
            echo '<head>';
            echo '<title>Вы заблокированы</title>';
            echo '<meta http-equiv="content-type" content="text/html; charset=utf-8" />';
            echo '</head>';
            echo '<body>';
            echo '<p style="background:#ccc;border:solid 1px #aaa;margin:30px au-to;padding:20px;text-align:center;width:700px">';
            echo 'Вы заблокированы администрацией ресурса.<br />';
            exit;
        }

        // Установка путей к директориям
        $path_active = self::pathActive;
        $path_block = self::pathBlock;

        // Приведение путей к директориям к абсолютному виду
        if (!self::pathIsAbsolute) {
            $path_active = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_active . '/');
            $path_block = str_replace('\\' , '/', dirname(__FILE__) . '/' . $path_block . '/');
        }

        // Проверка возможности записи в директории
        if (!is_writable($path_active)) {
            die('Директория кэширования активных пользователей не создана или закрыта для записи.');
        }
        if (!is_writable($path_block)) {
            die('Директория кэширования заблокированных пользователей не создана или закрыта для записи.');
        }

        // Проверка активных ip-адресов
        $is_active = false;
        if ($dir = opendir($path_active)) {
            while (false !== ($filename = readdir($dir))) {
                // Выбирается ip + время активации этого ip
                if (preg_match('#^(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})_(\d+)$#', $filename, $matches)) {
                    if ($matches[2] >= time() - self::intervalSeconds) {
                        if ($matches[1] == $ip_address) {
                            $times = intval(trim(file_get_contents($path_active . $filename)));
                            if ($times >= self::intervalTimes - 1) {
                                touch($path_block . $filename);
                                unlink($path_active . $filename);
                            } else {
                                file_put_contents($path_active . $filename, $times + 1);
                            }
                            $is_active = true;
                        }
                    } else {
                        unlink($path_active . $filename);
                    }
                }
            }
            closedir($dir);
        }

        // Проверка заблокированных ip-адресов
        $is_block = false;
        if ($dir = opendir($path_block)) {
            while (false !== ($filename = readdir($dir))) {
                // Выбирается ip + время блокировки этого ip
                if (preg_match('#^(\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3})_(\d+)$#', $filename, $matches)) {
                    if ($matches[2] >= time() - self::blockSeconds) {
                        if ($matches[1] == $ip_address) {
                            $is_block = true;
                            $time_block = $matches[2] - (time() - self::blockSeconds) + 1;
                        }
                    } else {
                        unlink($path_block . $filename);
                    }
                }
            }
            closedir($dir);
        }

        // ip-адрес заблокирован
        if ($is_block) {
            header('HTTP/1.0 502 Bad Gateway');
            echo '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
            echo '<html xmlns="http://www.w3.org/1999/xhtml">';
            echo '<head>';
            echo '<title>502 Bad Gateway</title>';
            echo '<meta http-equiv="content-type" content="text/html; charset=utf-8" />';
            echo '</head>';
            echo '<body>';
            echo '<h1 style="text-align:center">502 Bad Gateway</h1>';
            echo '<p style="background:#ccc;border:solid 1px #aaa;margin:30px au-to;padding:20px;text-align:center;width:700px">';
            echo 'К сожалению, Вы временно заблокированы, из-за частого запроса страниц сайта.<br />';
            echo 'Вам придется подождать. Через ' . $time_block . ' секунд(ы) Вы будете автоматически разблокированы.';
            echo '</p>';
            echo '</body>';
            echo '</html>';
            exit;
        }

        // Создание идентификатора активного ip-адреса
        if (!$is_active) {
            touch($path_active . $ip_address . '_' . time());
        }
    }
	
    /**
    * Метод получения текущего ip-адреса из переменных сервера.
    */
    private static function _getIp() {

        // ip-адрес по умолчанию
        $ip_address = '127.0.0.1';

        // Массив возможных ip-адресов
        $addrs = array();

        // Сбор данных возможных ip-адресов
        if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            // Проверяется массив ip-клиента установленных прозрачными прокси-серверами
            foreach (array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $value) {
                $value = trim($value);
                // Собирается ip-клиента
                if (preg_match('#^\d{1,3}.\d{1,3}.\d{1,3}.\d{1,3}$#', $value)) {
                    $addrs[] = $value;
                }
            }
        }
        // Собирается ip-клиента
        if (isset($_SERVER['HTTP_CLIENT_IP'])) {
            $addrs[] = $_SERVER['HTTP_CLIENT_IP'];
        }
        // Собирается ip-клиента
        if (isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP'])) {
            $addrs[] = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
        }
        // Собирается ip-клиента
        if (isset($_SERVER['HTTP_PROXY_USER'])) {
            $addrs[] = $_SERVER['HTTP_PROXY_USER'];
        }
        // Собирается ip-клиента
        if (isset($_SERVER['REMOTE_ADDR'])) {
            $addrs[] = $_SERVER['REMOTE_ADDR'];
        }

        // Фильтрация возможных ip-адресов, для выявление нужного
        foreach ($addrs as $value) {
            // Выбирается ip-клиента
            if (preg_match('#^(\d{1,3}).(\d{1,3}).(\d{1,3}).(\d{1,3})$#', $value, $matches)) {
                $value = $matches[1] . '.' . $matches[2] . '.' . $matches[3] . '.' . $matches[4];
                if ('...' != $value) {
                    $ip_address = $value;
                    break;
                }
            }
        }

        // Возврат полученного ip-адреса
        return $ip_address;
    }
	
    /**
    * Метод проверки на поискового бота.
    */
    private static function is_bot()
    {
		if (!empty($_SERVER['HTTP_USER_AGENT'])) {
			$options = array(
				'YandexBot', 'YandexAccessibilityBot', 'YandexMobileBot','YandexDirectDyn',
				'YandexScreenshotBot', 'YandexImages', 'YandexVideo', 'YandexVideoParser',
				'YandexMedia', 'YandexBlogs', 'YandexFavicons', 'YandexWebmaster',
				'YandexPagechecker', 'YandexImageResizer','YandexAdNet', 'YandexDirect',
				'YaDirectFetcher', 'YandexCalendar', 'YandexSitelinks', 'YandexMetrika',
				'YandexNews', 'YandexNewslinks', 'YandexCatalog', 'YandexAntivirus',
				'YandexMarket', 'YandexVertis', 'YandexForDomain', 'YandexSpravBot',
				'YandexSearchShop', 'YandexMedianaBot', 'YandexOntoDB', 'YandexOntoDBAPI',
				'Googlebot', 'Googlebot-Image', 'Mediapartners-Google', 'AdsBot-Google',
				'Mail.RU_Bot', 'bingbot', 'Accoona', 'ia_archiver', 'Ask Jeeves', 
				'OmniExplorer_Bot', 'W3C_Validator', 'WebAlta', 'YahooFeedSeeker', 'Yahoo!',
				'Ezooms', '', 'Tourlentabot', 'MJ12bot', 'AhrefsBot', 'SearchBot', 'SiteStatus', 
				'Nigma.ru', 'Baiduspider', 'Statsbot', 'SISTRIX', 'AcoonBot', 'findlinks', 
				'proximic', 'OpenindexSpider','statdom.ru', 'Exabot', 'Spider', 'SeznamBot', 
				'oBot', 'C-T bot', 'Updownerbot', 'Snoopy', 'heritrix', 'Yeti',
				'DomainVader', 'DCPbot', 'PaperLiBot'
			);
	 
			foreach($options as $row) {
				if (stripos($_SERVER['HTTP_USER_AGENT'], $row) !== false) {
					return true;
				}
			}
		}
	 
		return false;
	}

}

// Проверка текущего ip-адреса
BotBlockIp::checkIp();

Для установки скрипта:

  • создаём папку, например block;

  • в ней создаём папки active и block;

  • создаём php-файл с вышеприведённым скриптом, например bot_block_ip.php;

  • на любом сайте, в индексном файле, в самом начале подключаем наш скрипт:

    Например, в Битрикс я разместил скрипт в папке tools:

<?include($_SERVER["DOCUMENT_ROOT"]."/bitrix/tools/block/bot_block_ip.php");?>

Скрипт прекрасно работает на php 7.4. Анализируя логи сервера стал замечать, что атакующие боты стали делать паузы между запросами (раньше доходило до 10-20 запросов в секунду, сейчас некоторые боты стали делать 1-2 запроса в секунду-две) и было решено - отсечь трафик не из России. Конечно пользоваться vpn и proxy никто не запрещает, но доля "мусорного" трафика сошла почти на нет. Если этот пост заинтересует достаточное количество людей, то напишу в следующих постах о доработанной версии этого скрипта, который блокирует все запросы не из России например (можно любую страну выбрать).

Спасибо, что прочитали. Сильно не критикуйте, скрипт работает, что от него и требовалось :-)

Комментарии (63)


  1. Expany
    08.04.2022 12:36
    +14

    Имхо, но мне кажется такие вещи стоит делегировать на iptables(или аналог современее) и fail2ban, как минимум!

    Между запросом и php проходит уйма событий и слоев, а это время и ресурсы.

    Плюс, хотелось бы услышать профит возможно бенчмарки по использованию скопа echo, вместо единичного вызова.


    1. Lure_of_Chaos
      08.04.2022 16:09
      +1

      Между запросом и php проходит уйма событий и слоев, а это время и ресурсы.

      соглашусь полностью, задача DoS - забить канал, мера противодействия также должна быть уровнем как можно ниже, иначе мы даже облегчим задачу

      в сторону отмечу, что для повышения устойчивости необходимо "срезать углы" на всех этапах:

      • клиентское кэширование: если можно обойтись без запроса на сервер, то лучше сервер не трогать

      • на сервере - сначала применить низкоуровневые проверки, те же iptables/fail2ban

      • затем применить то же кэширование, желательно на уровне прокси (тот же nginx и т.д.)

      • затем стараемся выполнить работу на уровне программного кода и памяти, не обращаясь к базе данных, файловой системе и другим "третьим лицам"

      • при обращении к ним также используем кэши (1,2, 3 уровней, ну вы знаете, для каждой платформы и базы данных свои)

      т.е. даже (без переписывания на ассм и С) используемая технология (платформа, язык фреймворк) должны обеспечивать наименьший, как я это называю, "путь запроса", включающий в себя все инициализации и проверки (особенно важно отметить здесь проверки безопасности)

      отлично, если мы обходимся самым низким уровнем (к примеру, отдаём файлы серверным ПО, а не скриптом, тянуть такое добро из БД вообще имхо расточительно)

      к сожалению, низкий уровень и проверки безопасности (например, при регистрации нового пользователя мы вряд-ли будем заводить такого в системе - что даже более опасно - чтобы проверить без БД, имеет ли он право к таким файлам) стоят (как правило) на разных уровнях и "тянут одеяло на себя", но без последних вообще нельзя...

      ...но тем не менее, чем популярнее ресурс, тем (больше нагрузка) важнее минимизировать ресурсы, выделяемые на обработку запроса (не в ущерб безопасности и адекватности)

      и с таких позиций реализация средств контроля доступа (таких низкоуровневых, как анти-ддос) на высоком уровне становится смехотворно непрактичной.


      1. mSnus
        08.04.2022 21:52
        +1

        fail2ban тоже очень быстро начинает захлёбываться и получается отличный DoS


    1. FanatPHP
      08.04.2022 18:13
      +1

      Ой, я вас умоляю. Нет там никакого профита. Никакой осмысленный бенчмарк не покажет разницы даже близко, там погрешность измерения будет на порядки больше измеряемых значений. Бенчмаркать надо бутылочные горла, а не рандомные фантазии.


      В остальном все верно, на РНР такую защиту делать как минимум бессмысленно, а уж в таком исполнении — так и вовсе будет приносить больше вреда, чем пользы.


  1. kulaginds
    08.04.2022 12:37
    +1

    Спасибо за статью. Подскажите, не рассматривали ли вы готовые решения, например fail2ban?


  1. alfa
    08.04.2022 12:40
    +1

    Для блокировок по странам есть geoip_module в nginx если выносить на уровень веб-сервера, зачем делать этот слой на php?


    1. kibershot
      08.04.2022 12:49
      -4

      на случай если сайт на хостинге простом а не на VDS


      1. alfa
        08.04.2022 12:53
        +3

        Тогда защита от ддос перекладывается на плечи хостера. Поднимать пхп со всеми модулями, чтобы проверить ip, ну такое. С таким же успехом можно грепать логи и заполнять .htaccess, но не по людски это.


  1. Akuma
    08.04.2022 12:44
    +17

    "Обожаю" такие сайты: пытаешься открыть несколько вкладок, а оно тебя блочит по IP :)


  1. Deny_83 Автор
    08.04.2022 12:45
    -2

    Отвечу сразу на первые комментарии:

    fail2ban реализуется средствами сервера, а доступ как правило есть не всегда - например хостинг не VPS/VDS, где вы не можете редактировать конфы и устанавливать доп.модули linux.

    Что касаемо "скопа echo" - не важна реализация вывода, можно и одним exit или die заменить, просто для наглядности выводится мало-мальски оформленный в html текст. Скрипт очень быстрый, т.к. не вызывает никаких сторонних модулей и отрабатывает "в себе".

    Данный скрипт - превентивная мера, нежели полноценная защита, и имеет место быть, как для познания, так и для использования, вреда точно от него нет :-)


    1. alfa
      08.04.2022 12:54
      +2

      " а доступ как правило есть не всегда - например хостинг VPS, где вы не можете редактировать конфы и устанавливать доп.модули linux."

      Как раз наоборот, на VPS можете, это на шареде "нюансы".


    1. Expany
      08.04.2022 13:28
      +2

      - На шареде есть саппорт, это их задача и ниша, в крайнем случае, между доменом и шаредом подставляется ddos-guard/cloudflare, чья задача как раз в том, что бы "превентивно" отсеивать сомнительный трафик.
      - Скоп echo не дает никакого профита, можно было оформить меньшим числом вызовов, что и стоило сделать, в виду "очень быстрый".
      - Данный скрипт - излишняя мера усложнения, которая может быть уместна исключительно в вопросе изучения, но абсолютно не применима в реальных условиях.

      В созидательных целях, материал приемлем, применять его, лично я, не советовал бы.


    1. Lure_of_Chaos
      08.04.2022 21:01
      +4

      fail2ban реализуется средствами сервера, а доступ как правило есть не всегда - например хостинг не VPS/VDS, где вы не можете редактировать конфы и устанавливать доп.модули linux.

      имхо, если вы настолько бедны, что не можете позволить себе "настоящий хостинг", то вас также не должны волновать вопросы подобно сабжу.

      обычно бесплатный/дешевый хостинг несовместимы со "значимыми"/"серьезными" проектами. когда выйдете на новый уровень - у вас будут и эти проблемы, и деньги для их решения.

      серьезно. да, бывают атаки и на странички Васи Пупкина, но должны ли вы об этом беспокоиться? по-моему, нет. а вот когда вы представляете коммерческую/общественную организацию, тогда, как правило, у вас есть деньги на хостинг подороже (а то и уже с нужными средствами "из коробки")


  1. pewpew
    08.04.2022 12:48
    +1

    было решено — отсечь трафик не из России
    Заходишь на такой сайт с VPN и ой… Знаете, а это даже хорошо. Поменьше будет посетителей у таких сайтов.
    По существу — пробовал подобное решение. Справляется только со слабым DDoS. Для более плотного и разнообразного потока трафика лучше использовать низкоуровневые средства.


  1. cjbars
    08.04.2022 13:54
    +12

    Нападает на крепость толпа зомбаков.
    - крепостные ворота их пропустили
    - по улицам пройти их пропустили
    - в дом зайти им позволили
    и вот тут уже в прихожей мы херачим их скалкой.

    Может таки низкоуровневые атаки делегировать более низкому уровню?


  1. Kliba
    08.04.2022 13:55
    +4

    Я конечно не php-шник, но мне кажется или для каждого заблокированного IP там создается отдельный файл? И соответственно это такой изощренный способ уложить не только веб-сервер, но и VPS\VDS целиком, тупо забив inodes любой сколько-нибудь серьезной DDoS-атакой с ботнета? Поправьте, плиз, если я ошибаюсь.


    1. andreymal
      08.04.2022 13:59
      +3

      IP-адрес считывается из HTTP-заголовка Client-IP, так что даже ботнета не надо — с этим справится один простенький Python-скрипт в десяток-другой строк, подставляющий рандомные IP-адреса в заголовки запроса


    1. Deny_83 Автор
      08.04.2022 14:23

      Да, верно, для каждого ip создаётся файлик, и сам удаляется. Это с целью упрощения и не поднимая mysql, тогда бы сложнее было и дольше, как писали выше "несколько слоёв".

      Любая серьёзная атака положит сервак нодами. А сам скрипт для небольших проектов, например ставлю на маленькие интернет-магазины или корп.порталы битрикс, чтобы на корню пресечь бота и не дёргать битрикс-ядро, которое само по себе тяжёлое.


      1. alfa
        08.04.2022 15:45
        +2

        Соответственно создав простым скриптом, [сарказм] даже на php [/сарказм] передав миллион запросов с фейковыми CLIENT_IP мы получим миллион файлов, чему безумно будет рад хостер в попытках забэкапить это, будет рад php (и апач с ним) в попытках удалить файлы, ну и по мелочам.


        1. andreymal
          08.04.2022 15:52

          Скрипт удаляет старые файлы, созданные больше секунды назад (const intervalSeconds = 1), что несколько затрудняет создание миллиона файлов


          1. oxidmod
            10.04.2022 13:58
            +1

            Множественные перации с фс на каждый запрос... Красота!


  1. saipr
    08.04.2022 14:30

    было решено — отсечь трафик не из России

    У нас была проблема после начала известных событий и трафиком не из России. Все проблемы решил iptables.


  1. Gippsland
    08.04.2022 14:50

    Не знаю и чего все так набросились. Как говориться - "все методы хороши".

    на случай если сайт на хостинге простом а не на VDS

    Поддерживаю.


  1. ReDev1L
    08.04.2022 16:31
    +9

    Будущее российского ИТ)


  1. Aina23
    08.04.2022 16:52
    +1

    $_SERVER["REMOTE_ADDR"] - единственный верный ip адрес.Все остальные которые клиент передает по своему усмотрению не в коем случае в расчёт принимать не нужно.


  1. olegtsss
    08.04.2022 17:06
    +1

    А по-моему прикольный костыль. Сам придумал, сам сделал, для конкретной задачи подходит. Не рационально, но и пусть. На то он и костыль.


    1. FanatPHP
      08.04.2022 17:29

      Какой задачи?


      1. olegtsss
        09.04.2022 06:21

        Защита от DDOS, парсинга и ботов на PHP.


        1. FanatPHP
          09.04.2022 09:15
          +2

          Первая буква D в слове DDOS означает распределённая. То есть нагрузка идёт с разных IP. И данный скрипт будет только ухудшать ситуацию, добавляя к потерям производительности от собственно атаки еще и потери на перебор и удаление миллиона файлов при каждом запросе.
          Эффективно бороться с DDOS можно только на уровне провайдера. Делать это на уровне приложения — это тушить огонь бензином.
          Не говоря уже о том, что его сайт никогда ни DDOS-у, ни просто DOS-у не подвергался. И у автора нет ни одного реального подтверждения того, что этот говнокод хоть от как-то защищает от атак на отказ в обслуживании.


          От парсинга этот скрипт вообще не защищает, поскольку ему можно просто подсовывать новый рандомный IP адрес в НТТР запросе, не меняя реального хоста с которого идёт парсинг. А вот кому этот скрипт реально нагадит — это честным пользователям.
          Плюс опять же увеличит нагрузку на пустом месте, создавая 100500 файлов для фейковых IP. В случае, если его реально начнут парсить.


          Этот скрипт не защищает от DDOS, парсинга и ботов. Он защищает от влажных фантазий автора про "DDOS, парсинг и ботов".


          Его идеи про то, что скрипт "работает" — это такие же фантазии. Всё что он видит в своих логах — это гребущие широким неводом скрипт-кидди, всё время разные, которые сканируют целые сети, и которым совершенно насрать на его сайт. И которые даже не подозревают, что этот сайт как-то там копошится у себя в ответ на их бездумное сканирование.


          Вся эта история вообще не про защиту. Она укладывается в три строчки:


          • Посмотрел в логи, увидел много мусора. На работе сайта не отражается никак, но раздражает.
          • Написал какой-то код, разместил на сайте. Доволен, как слон
          • Посмотрел в логи, увидел много мусора. Разницы в работе сайта как не было, так и нет. Но раздражает уже не так сильно!

          Если вдуматься, то чувак воюет не с ботами. А с логами на своем сервере.
          Если бы статья была про ботов, то в ней были бы приведены реальные результаты этой борьбы, которые реально отражаются на работе сайта. А не "я заметил в логах".


          1. olegtsss
            09.04.2022 10:52

            Я согласен со всем, что вы написали. Но все равно считаю, что подобного рода костыли имеют право на существование. Вы привели минусы, для решения поставленной задачи. Плюсов вроде не видно. Однако через такие тернии можно прийти и к толковым решениям.


  1. Rober
    08.04.2022 17:46

    На первый взгляд скрипт полезен для хостингов, которые умеют отсекать DDoS, но не следят за ботами и не дают никаких низкоуровневых инструментов. Однако реализация не оптимальна, хотя бы потому что первой проверкой идёт определение "доброжелательности" бота по подстроке в UserAgent без всяких forward DNS lookup-ов или хотя бы белых списков IP. Слишком легко обходится. Неужели никто не встречал GoogleBot-ов, запущенных с серверов DO? Другие странные моменты и так уже расписали, повторять не буду.

    Для шаред-хостинга, в принципе, такой скрипт (по концепции, а не этот) имеет право на жизнь, однако если есть возможность, лучше подключить что-то вроде https://github.com/mitchellkrogza/nginx-ultimate-bad-bot-blocker или вовсе поставить OpenLiteSpeed, который умеет отдавать капчу при попытках забросать сайт запросами.


  1. FanatPHP
    08.04.2022 17:56
    +3

    Главное, что я не понял — так это смысл этого кода. Его предназначение.
    Вначале говорится про логи веб-сервера. Но логи веб-сервера облагораживать на РНР глупо — запрос уже пришёл, в лог запись попала. Если уж чистить логи — то в конфиге веб-сервера.
    Защита от взлома? Этот скрипт не занимается защитой от взлома. Не будем же мы всерьёз рассматривать скрипт-киддей, ищущих уордпресс?
    DDOS — это конечно главная стратегическая ошибка автора. Если бы этого красивого и загадочного слова не было в заголовке, то читатели отнеслись бы к статье гораздо снисходительнее.
    Остаются парсинг и боты. Я ОЧЕНЬ сильно сомневаюсь, что этот скрипт когда-либо применялся на сайте, который в реальности подвергается серьезным атакам ботов или пытается оберегать драгоценную информацию от парсинга. Просто потому что это конечно слёзы, а не защита.


    В общем, с одной стороны — это неплохой код для джуниора (кто сам не выковыривал IP адрес из НТТР заголовков, пусть бросит в меня камень). И автора надо похвалить хотя бы просто за то что он задумал и реализовал такой проект.
    Но с другой стороны — весь тот проект построен не на реальных потребностях, а на совершенно диких фантазиях, не имеющих ничего общего с реальностью. И это реальная проблема. Таких фантазёров, увы, сейчас много, люди отрываются от реальности и делают вещи, которые не имеют ни малейшего смысла. Такие симулякры.


    Ну и пара чисто технических замечаний. В коде есть пара мест, от которых у меня сводит зубы. Про получение IP адреса уже написали 10 раз, на этом останавливаться не будем. Следующим по вредительству у нас идет цикл с перебором ВСЕХ файлов на каждый запрос. Я понимаю что нужна чистка мусора, но её-то надо как раз выносить отдельно. А этот скрипт, который должен по идее облегчать работу сайта, НЕ ДОЛЖЕН проедать борозды на жёстком диске при каждом обращении. Если уж искать файл, то точечно, по совпадению айпи. А иначе, вместо ожидаемого во влажных мечтах уменьшения нагрузки, в один прекрасный день сервер встанет колом. То же самое касается и остальных циклов, нагрузка копеечная но все равно, на пустом месте менять O(1) на O(n) не стоит.


    Дальше. Вот это вот скараментальное


    die('Директория кэширования активных пользователей не создана или закрыта для записи.');

    — вот кому, КОМУ мы это пишем? Атакующим злодеям? Чтобы они надорвали животики, если случайно увидят этот лепет своих логах? Нет? А кому тогда?
    Не пора ли уже понять наконец, что боевой сайт — это не домашний компик, за которым ты и программист, и единственный пользователь. И все эти die как минимум совершенно бессмысленные, а как максимум — несут конкретный вред, сообщая нехорошим людям информацию о внутреннем устройстве системы.
    Надо потихоньку открыть для себя, что ВСЕ системные ошибки на боевом сайте пишутся только в лог, и никогда не выводятся клиенту.


    Сами по себе эти проверки бессмысленные и вредные. Никогда не нужно подменять подробное, адекватное, содержащее тонну отладочной информации системное сообщение об ошибке на невнятный пересказ. Если выкинуть эти бессмысленные проверки, то РНР подробно напишет — ПОЧЕМУ он не смог открыть файл, и КАКОЙ ИМЕННО.


    str_replace('\\' — зачем здесь этот карго-культ? В чем смысл этих глубокомысленных телодвижений? Зачем что-то заменять в полученном от операционной системы имени файла? Чем исходный путь не устраивает?


    Мне кажется, если бы Хабр сделал сервис для code review, то можно было бы канализировать энергию джунов в совершенно другое русло. Было бы и больше постов, было бы больше отзывов, было бы больше пользы и было бы гораздо меньше негатива от тех, кто пришел почитать на Хабр адекватные материалы, а не первые пробы робкого пера.


    1. Deny_83 Автор
      08.04.2022 18:43
      +1

      Спасибо коллега! Впечатлило, единственный стОящий комментарий на мою статью.

      Чуть поясню, этому коду уже лет 5-6, но работает. Применял я его редко, но и по сей день фильтрует от "свидетелей вордпресс", а чаще просто стопорит частые запросы страниц.

      В полной версии кода: ежесуточно скачивает базу ip (cidr) российскую, делает её бэкап на случай, если в следующие сутки не скачается новая, и проверяет адрес по диапазону, если не подошёл, блок. Прошу, ничего не говорить и об этой фишке, сам знаю :-) Но, по факту, на нескольких малонагруженных проектах это работает и периодическая проверка логов на шареде только подтверждает паломничество "свидетелей вордпресс".

      Собственно, много лет назад, только из за них пришлось лепить костыль. Просто бесило, что сайт каждый день атакуют боты, с целью ломануть вордпресс, а я такой "типа cms" отродясь не пользовался.

      Код ни на что не претендует, был выложен на кодревью чисто для получения приглашения, чтобы в дальнейшем что то дельное написать. Но, чувствую, забанят меня, столько минусов, поднялся в рейтинге до наоборот :-)


      1. FanatPHP
        09.04.2022 09:40
        +2

        Мне очень нравится ваш оптимизм, но настораживает, что вас не тошнит от своего же кода шестилетней давности. Любого нормального разработчика должно. Это очень, очень плохой признак.


        Отсечка по базе IP адресов и уж тем более регионам — это совершенно другая задача, которая вообще не требует кода на пэхапе, а делается штатными средствами веб-сервера.


        Ну и повторюсь — сама по себе задача отлова в логах "свидетелей вордпресс" мне кажется очень сомнительной, и совершенно не стоящей каких бы то ни было усилий.


        На случай если вы неверно меня поняли — на Хабре пока нет сервиса для code review, и выкладывать статьи для этих целей — гарантированно получить минусов полную панамку.


        1. Deny_83 Автор
          09.04.2022 10:12

          Ещё раз благодарю за адекватную оценку. Хотя оптимизма здесь нет, я реалист. А что касается тошноты кода, то здесь как то равнодушно отношусь к этому. Код для "школьника", соблюдены все каноны (табы с комментами).

          Скрипт забавы ради, но работает же. Вчера словил некоторый траффик отсюда, всем спасибо, краш-тест пройден :-)

          Скрипт собственно и задумывался, чтобы отсечь "свидетелей вордпресс", а не фильтрации ip и стран (это я в рабочем проекте докрутил этот код).


  1. rudinandrey
    08.04.2022 18:30

    делал на своем сайте подобное, код не читал, потому что бессмысленно... в общем тоже смотрел, если с одного IP прилетает несколько запросов за короткое время, то этот IP в бан отправлял, но у меня еще каждому клиенту отдается случайный токен, по которому я сравниваю следующие запросы, если IP одинаковый, токен одинаковый, то значит это просто кто-то возможно быстро смотрит, если токены разные с одного IP адреса, значит это какой нибудь file_get_contents($URL) который куки не хранит между запросами. В итоге на время количество запросов снизилось, а потом они все равно адаптировались ) меняли IPшники, это уже никак не отследишь, кто его знает, хороший там пользователь за этим IP адресом или плохой. в общем на этом я остановился.

    Подключать всякие file2ban не хочется, ими нужно заморачиваться, понимать как они работают, потому что если что-то пойдет не так, ты будешь думать кто виноват это или это.


  1. NikaLapka
    08.04.2022 19:53
    -4

    Поддержу автора, мне очень понравилось решение, и сам код легко читаем для новичков, красиво - правильно оформлен.

    веб сервер с уже установленным языком сценариев вполне самодостаточный для какой-то защиты, поэтому fail2ban считаю избыточным, лишним, и это прекрасный пример в статье. а вот как блокировать, что выдавать, будете ли вы просто писать в файлики, или sqlite, или может быть "error_page 301 400 403 404 500 502 503 504 =444" и т.п. это на усмотрение автора, но файлики мне тоже очень нравятся, наглядно и просто.

    НЕ ДОЛЖЕН проедать борозды на жёстком диске при каждом обращении

    Если стоит выбор между удобством, наглядностью, простотой и при этом вы используете 1% ресурсов предоставляемого хостинга, то нет смысла усложнять работу, чтобы снизить нагрузку до 0.5%. Т.к. плата за борозды уже включена в ежемесячные платежи. Хороший пример - популярность докер контейнеров.


    1. FanatPHP
      08.04.2022 20:39
      +2

      Вам тоже задам вопрос, какое решение? Решение какой именно задачи?


      Чушь про "выбор между наглядностью" даже комментировать не буду.


    1. dopusteam
      08.04.2022 20:45
      +2

       красиво - правильно оформлен.

      Нет


  1. Dimasik30
    08.04.2022 19:59
    +1

    Лучше отдавать 502ю ) Бот подумает что вы лягли и перестанить ddos )


    1. tuxi
      08.04.2022 20:40
      +1

      нет, лучше отдать 503, так как можно указать retry after


    1. mSnus
      08.04.2022 22:11
      +1

      418-ю, бот подумает, что это чайник и перестанет ddos!


      1. tuxi
        08.04.2022 22:26
        +1

        нет, не перестанет. это из моего 10-ти летнего опыта практических занятий по этой теме)))

        можно «пощупать» уровень интеллектуальности бота-дятла, отправив ему 301/302/308 с указанием локейшена на 10 или 100 гигабайтный файл (есть такие, для проверки скорости интернета) где нибудь в голландии или колумбии.
        Если захлебнется через час, значит ботовладелец исчерпал место у себя на VPS.
        Но так делать не надо, это нехорошо, не надо быть редиской!!! Это чисто для анализа ботов способ.


        1. Expany
          09.04.2022 15:13

          а zip\gzip-бомбы не актуальны?


          1. tuxi
            09.04.2022 16:03

            так делать не надо, это нехорошо, не надо быть редиской!!!


            1. JerleShannara
              09.04.2022 17:01

              add_header X-Warn-Bots BotsWillBeKilled always;
              Вроде как предупредили ботов, а то, что они не читают этот заголовок — «С моей стороны пули вылетели. Проблемы у вас».


          1. IlliaHai
            10.04.2022 21:12
            +1

            Нет, конечно.

            Бот же не будет распаковывать архив, он его просто скачает.


            1. FanatPHP
              10.04.2022 23:09
              +1

              Архивы тут не при чем, речь про сжатие НТТР трафика.


              1. JerleShannara
                11.04.2022 00:42

                А если ещё подвесить на отдачу такого gzip-нутого http потока ограничение скорости в 10-20Кбит/с, то становится весело (пока не понимаешь, что у тебя на сайте висит 5000 ботов и жрут ресурсы кол-ва соединений).


  1. Upsarin01
    08.04.2022 19:59

    Один из базовых способов обхода защиты, указать в user agent, что ты бот гугла или яндекс. Хотя раньше такое срабатывало, сейчас редко, банят по диапазону ip


  1. tuxi
    08.04.2022 20:36
    +2

    Часть запросов поисковые боты делают без указания своего UA, именно по причине выявления случаев, когда сервер отдает разный контент для бота и для живого посетителя.

    Поэтому, пока еще ваш проект не потерял позиции в гугле/яндексе, изучите nslookup, чтобы более достоверно определять apple/google/yandex/msn/petalsearch/mailru поисковых ботов.

    После этой реализации, вы получите моральное право записать в черный список тот айпи, у которого UA прикидывается поисковым ботом, но не является им по сути.


  1. sden77
    08.04.2022 21:55

    Если стоит задача бороться с парсингом, то без списков айпи адресов датацентров и известных ботов вам не обойтись. Для себя я периодически покупаю свежие базы данных udger.com, это стоит 40 долларов в месяц, но если нет серьёзных на то причин, особого смысла получать ежемесячные оновления я не вижу, вполне достаточно обновляться 1-2 раза в год. Можно эти данные и самому собирать, списки автономных систем есть в открытом доступе, плюс можно найти дополнительные истчники данных типа выходных нод тора, открытых прокси и т.д.


  1. mSnus
    08.04.2022 22:44
    +1

    Получился велосипед, который довольно медленно работает (хотя и быстрее, чем неповоротливый Битрикс).

    Обойти его довольно несложно, например, насколько я понимаю, отдать в запросе "127.0.0.1" в качестве REMOTE_ADDR не проблема, если вам не нужно получать ответ сервера. Или просто прикинуться ботом.

    В целом код такой тоскливо-ностальгический, все когда-то (лет 10 назад) писали что подобное, наверное. Может даже с echo в каждой строчке. Но вряд ли кто-то этим гордился и выкладывал.

    Сейчас вместо таких велосипедов есть github и соответствующий топик на нём, там много разных интересных решений. Почитайте, как это решается, это намного полезнее код-ревью данного велосипеда.


    1. FanatPHP
      09.04.2022 09:32

      Я бы не сказал что "отдать в запросе 127.0.0.1 в качестве REMOTE_ADDR" это "не проблема". Хотел бы я посмотреть на того героя, который возьмётся провернуть этот фокус. Думаю, вы перепутали REMOTE_ADDR с одним из НТТР заголовков. В которые действительно можно насовать чего угодно, и обойти эту "защиту" как два пальца.


      1. mSnus
        09.04.2022 15:09

        Мне кажется, что на уровне TCP это всё же можно подменить, просто ответ тогда не получишь. Но, может, вы и правы, проверить быстро не могу.

        Другое дело, что в данном скрипте достаточно подменить HTTP_X_FORWARDED_FOR, чтобы функция вернула его в getIp() - хоть 0.0.0.0.


        1. FanatPHP
          09.04.2022 20:55

          Подменить можно, но дальше компьютера пакет с адресом 127.0.0.1 не уйдет. Всё дело в нём.
          Какой угодно другой — пожалуйста. Если подменить на уровне TCP исходящий адрес на любой публичный IP, то вот тогда все так и будет как вы написали — запрос дойдет, адрес будет поддельный, но ответа не получишь.


          1. mSnus
            09.04.2022 22:08

            Интересно, спасибо.


          1. JohnDidact
            09.04.2022 22:12

            Я из-за тебя на стройку пошёл (((


  1. amarao
    09.04.2022 22:18
    +4

    Спасибо за поддержание репутации PHP на соответствующем PHP уровне. А то меня facebook с их hiphop начал смущать. Вижу, PHP, написан как PHP, работает как PHP.


  1. Upsarin01
    09.04.2022 23:16
    +2

    Короче, был лет так 10 назад в дорвеях такой генератор, кажется sed назывался. Он парсил выдачу поисковиков и на этом контента формировал страницы. Ну так вот там был забавный баг, который видимо есть и в этом коде.

    В генераторе доров это была очистка кэша когда он переваливал за определённое число файлов и по времени создания файлов. Приблизительно так же как и сейчас реализовано у Вас.

    Ну так при кэшах более 25000 файлов, убивался сервер. Так как каждый раз при создании новой страницы, запускалась процедура очистки кэша. А opendir на такой объем работает нп мгновенно. Тогда ещё и ssd не было.

    Если в секунду создаётся хотя бы 2-3 страницы, сервер падал. Исправлялось это все ограничением запуска этой процедуры, раз в 15 минут.

    Возвращаюсь к вашему коду, вы делаете проверку при каждом обращении. Будет ли такая система способна обработать хотя бы при 5000 уникальных обращениях в минуту?

    Представьте, 2 минуты нагрузка 5000 уникальных ip, создаётся 5000 файлов. При повторном обращении, скрипт должен пробежать по всем 5000 файлам, чтобы найти нужный и убрать просроченные.

    Ну то есть 5000 обращений хотя бы по 2 раза, за 2 мин, потребуют 5000*5000 чтений файла. Есть вероятность что сам скрипт прикончит сервер а не сама нагрузка.

    Я конечно не эксперт по php, но предположу что нужно функцию проверки разбить хотя бы на две:

    Очистка кэша и Проверка ip хороший или плохой.

    Мне не нравится формат хранения ip, с датой на конце, не проще ли хранить эту информацию в файле?

    Проверка была бы такой (обычно в кэши я люблю делать md5 от строки, чтобы не бороться со спец.символами и хацкерами):

    If (is_file(md5($request_ip))) {

    Прочитать данные и вернуть True или false

    } else{

    Сохранить данные

    }

    Предполагаю что такая проверка будет работать на порядок шустрее opendir. И может спокойно выдержать и 5000 и 10000 запросов.

    И отдельно раз в 1 минуту очистка данных.

    Кроме этого, есть нюанс по поводу того, что если допустим бот будет обращаться к сайту в 1 секунду, 59 секунду, а потом в 61,62,63 секунду, то он должен быть забанен, так как в интервале между 59 и 63 секунде было 4 обращения, но этого скорее всего не случится. Но это уже нюансы. Так как это усложенние скрипта и хранения интервалов.

    А так по хорошему, хэш от ip+browser, в файл писать построчно интервалы обращений, а при проверке проверять за последних 60 сёк, сколько было обращений.

    ЗЫ. Ни на что не претендую, просто взгляд со стороны.


  1. KivApple
    11.04.2022 01:46
    +2

    Хочется рассказать страшную тайну автору, что БД работает гораздо быстрее, чем каталог с несколькими тысячами файлов... Собственно, это одна из причин их использования, даже если часто не основная.


  1. A-TA-TA
    11.04.2022 09:18

    Только меня одного смущает, что часть кода не видно без танцев с бубнами через f12.


  1. GektorTM
    11.04.2022 10:03

    По поводу проверки ботов по user agent, такое себе решение, на моих сайтах 90% ботов имеющих гео локацию с китая и прочих стран, в которых никогда не было серверов гугла и яндекса, обладают строкой user agent яндекс и гугл ботов. Более того, с одного ip приходят как гугл так и яндекс боты ))). Так что без определения и проверки гео локации и подсетей поисковых ботов, эффективность решения очень слабое...