Бот в telegram, который следит за доменом

    Привет жителям Хабра.

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

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

    Исходный код в моем репозитории на гите.

    Мне захотелось использовать MVC структуру, что оказалось на мой взгляд не совсем верным, так как view не был задействован и это уже нельзя назвать mvc, но да ладно…

    Структура приложения




    Controllers — Связующее звено, соединяет model и логику приложения.
    Models — Файлы «бизнес-логики» приложения (Старался «запихнуть» сюда весь sql).
    Core — Файлы «ядра» приложения.
    Library — Библиотеки, использовал библиотеку для парсинга информации о доменном имени.

    Маршрутизация


    Файл маршрутизации (routes.php) расположил в каталоге core.
    В приложении добавлены 2 адреса:

    /bot — По этому адресу идет telegramm (необходимо установить webhook на этот адрес «uri/bot»).
    /check — По этому адресу ломиться wget с помощью cron 1 в день (в 12 часов), об этом чуть позже.

    BotController


    При переходе в данный контролер, получаем значения из тела POST запроса, и декодируем его в виде массива.

    json_decode(file_get_contents('php://input'), JSON_OBJECT_AS_ARRAY);

    php://input — получаем тело POST запроса

    CheckerController


    При переходе в данный контроллер, срабатывает скрипт, который проверяет все добавленные домены и ssl сертификаты на окончание срока действия, с интервалом:

    • текущая дата
    • 2 дня
    • 7 дней
    • 30 дней

    И отправляет уведомления если дата срока действия домена и ssl сертификата заканчивается.

    Добавление пользователей


    Когда пользователь пишет сообщение боту webhook telegram, отправляет его на наш сайт, далее нам приходит сообщение в json формате, которое нам нужно декодировать, и преобразовать в массив для дальнейшей работы.



    Будем работать с массивом message.

    Получаем из массива message['chat']: id , first_name
    где:

    id — идентификатор чата
    first_name — Имя пользователя

    А из массива message['text'], получаем отправленную пользователем команду.

    Находим пользователя в таблице users, если его нет, создаем

    
    // Ищем пользователя
    $sql= "SELECT user_id FROM users WHERE chat_id=?";
    $stmt = $db->prepare($sql);
    $stmt->execute([$chat_id]);
    $rows = $stmt->fetch(PDO::FETCH_ASSOC);
    return (int)$rows['user_id'];
    

    
    // Создаем пользователя
    $sql = 'INSERT INTO users (user_name, chat_id) VALUES (:user_name, :chat_id)';
    $insert = $db->prepare($sql);
    $insert->execute([':user_name' => $name, ':chat_id' => $chat_id]);
    return true;
    

    Добавление Доменов и SSL


    При отправке команды /addDomain url боту, получаем url домена из команды и получаем данные о регистрации домена с помощью этой библиотеки.

    Получаем домен


    Нам приходит ответ в виде текста:



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

    
    preg_match('/Registry\sExpiry\sDate:\s(.*)\\r/', $date, $matches);
    if (!$matches[1]){
        preg_match('/paid-till:\s*(.*)\\n/', $date, $matches);
    }
    $matches[1] = $this->formatDate($matches[1]);
    return $matches[1];
    

    Получаем SSL


    SSL сертификат решил получать с помощью openssl для linux.

    
    $getDomainSSL = shell_exec("echo | openssl s_client -servername $url -connect $url:443 2>/dev/null | openssl x509 -noout -dates");
    preg_match('~notAfter=(\w+)\s(\d+)\s.+\s(\d+)~', $getDomainSSL, $matches);
    $date =  $matches[2].$matches[1].$matches[3];
    $date =  date("Y.m.d", strtotime($date));
    $date = str_replace('.','-',$date);
    return $date;
    

    Таким образом получаем:

    echo | openssl s_client -servername google.com -connect google.com:443 2>/dev/null | openssl x509 -noout -dates
    notBefore=Jan  7 15:47:12 2020 GMT
    notAfter=Mar 31 15:47:12 2020 GMT
    

    Разбираем полученное с помощью регулярных выражений

    preg_match('~notAfter=(\w+)\s(\d+)\s.+\s(\d+)~', $getDomainSSL, $matches);



    Остается только добавить полученные данные в таблицу.

    
    $sql = 'INSERT INTO domains (domain_name, date_start, date_end, date_end_ssl) VALUES (:domain_name, :date_start, :date_end, :date_end_ssl)';
    $insert = $db->prepare($sql);
    $insert->execute([':domain_name' => $url, ':date_end' => $exp, ':date_end_ssl'  => $ssl_date]);
    

    Привязываем домен и ssl к пользователю


    Полученные данные нужно просто записать в промежуточную таблицу для того чтобы не «размножать» домены.

    
    $sql = 'INSERT INTO domain_users (user_id, domain_id) VALUES (:user_id, :domain_id)';
    $insert = $db->prepare($sql);
    $insert->execute([':user_id' => $user_id, ':domain_id' => $domain_id]);
    

    Проверка даты окончания домена


    Когда wget переходит по адресу /check, происходит выборка всех доменов и ssl сертификатов, у которых срок действия подходит к концу, и если такие есть, отправляет сообщение в чат к которому привязан данный домен.

    
    $db = $this->db;
    
    $sql= "
    	SELECT user_name, chat_id, domain_name, date_end 
    	FROM domain_users JOIN users USING (user_id)
    	JOIN domains USING (domain_id)
    	WHERE (
    		domains.date_end = CURDATE() OR 
    		domains.date_end = CURDATE() + INTERVAL 2 DAY OR 
    		domains.date_end = CURDATE() + INTERVAL 7 DAY  OR 
    		domains.date_end = CURDATE() + INTERVAL 30 DAY
    	)
    ";
    $stmt = $db->prepare($sql);
    $stmt->execute();
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
    

    CRON


    Крон в данном проекте нужен для организации перехода по адресу '/check', каждый день в 12 часов.

    crontab -e
    0 12 * * * wget url/check
    

    TOR PROXY


    Пару слов о отправки сообщений в telegram.

    К сожалению на территории мой страны (Россия), блокируют telegram, в том числе и его api.
    Приходится использовать прокси, мой выбор пал на tor proxy (еще с прошлого проекта).

    Его необходимо просто установить.

    sudo apt-get install tor

    Затем прокси будет доступен по порту 9050.

    
    curl_setopt($myCurl, CURLOPT_PROXYTYPE, 7);
    curl_setopt($myCurl, CURLOPT_PROXY, "127.0.0.1:9050");
    

    Всем спасибо за прочтение данной статьи!

    Эта моя первая статья, поэтому не судите строго :)

    Комментируйте данный пост, с удовольствием приму любую критику.

    Исходный код проекта в моем репозитории на github :)

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 23

      –1
      crintab -e
      crOntab -e
        +1
        спасибо, поправил.
        +1
        Для таких простых проектов вместо крона можно использовать таймеры systemd. Если скрипт в какой-то день не стартанёт, журнал systemd покажет в чём было дело.
          0
          systemd [irony]пока ещё[/irony] не везде используется.
            0

            Большое спасибо, буду знать :)

            +2
            preg_match('/Registry\sExpiry\sDate:\s(.*)\\r/', $date, $matches);
            Оххх, совсем не факт, что там будет именно эта строка, вариантов там десятки-сотни… в каждой зоне свой.
              0

              Согласен, но в свое оправдание скажу, вроде бы в РФ, в основном 2 агрегатора, reg.ru и nic.ru, у меня забито 2 регулярки на проверку именно 2 этих агрегаторов, таким образом, я думаю, получается покрыть до 90% сайтов.
              К сожалению я не особо представляю, как отказаться от регулярок.

                0
                Код подробно не смотрел, но рассмотрите вариант динамического встроенного теста: когда известные домены проверяются на известное же окончание их срока действия. И уведомление если они требуют внимания (ведь их тоже могут продлить).
                Это частично спасет от ситуации когда непонятно что с регуляркой (ибо регулярки зло, но очень заманчивое)
                  0

                  У меня реализовано это, как я понимаю.
                  Про что вы имеете ввиду, когда говорите "динамический встроенный текст", можно ссылку какую-нибудь :?)

                    0
                    Я имею в виду не теКСт, а теСт.

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

                    Еще можно использовать несколько разных (!) регулярок, которые должны поймать одно и то же… Но это меня понесло ))

                    Минус тут, разумеется, это требование по поддержанию актуальности дат экспирации этих тестовых доменов. Можно чуток схитрить, проверять еще и год — и если у вас ожидается дата 2020-01-29, то для даты 2021-01-29 или 2022-01-29 это будет не ошибка, а просто Warning с напоминанием что надо бы вручную проверить, что там на самом деле.

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

                    Вот примерно так.
                      0
                      у меня все обновляется раз в день, если дата подходит к концу
                  0
                  Доброе утро. Гляньте на этот проект.
                  github.com/io-developer/php-whois/blob/master/src/Iodev/Whois/Configs/module.tld.parser.block.json
                    0
                    Спасибо, в будущем обдумаю обновление!
                  0
                  Для всех gTLD это вполне даже факт — формат whois очень жёсткий и регламентирован ICANN, вариации могут быть только у ccTLD, но и их не так уж и много, может всего десяток и наберётся.

                  Другое дело что whois может уйти в прошлое в связи с появлением RDAP и планами ICANN перейти только на него, но там JSON так что дело упрощается.
                  +1
                  А есть упрощенный вариант своего бота для слежения за своими доменами?
                  Чтоб напоминал за n-дней до окончания срока?
                  Пробовал обычные скрипты типа:
                  github.com/Matty9191/dns-domain-expiration-checker
                  github.com/nixcraft/domain-check-2
                  и др., но ни один из них не работает с зоной UA.
                    0
                    У меня все это есть, и работает, можешь сделать себе pull, и изменить доменные зоны в библиотеки.
                    –1
                    Все очень и очень плохо. Про композер явно не слышали, проблему if/else не решили. Появится 100 команд, будете делать простыню из 100 if/else, switch/case? Одна статика во всех классах. Пытались решить полезную задачу, а код полезным не сделали.
                      +1
                      Дело в том что я только учусь как и писал в статье, этого бота сделал чисто для опыта и знаний, разницы нет switch / case или if / else, работает одинаково, да может не слишком симпатично.
                      Не очень понимаю что вы имели ввиду под названием «полезный код ?»
                      0
                      В попытках изучения PHP и долгих раздумьях

                      Только не в попытках, а в процессе :) Пыха простая, вторых и третьих попыток вряд ли будет.
                      Извините, что докапываюсь, я по-доброму. Просто слова «попытка», «долгие раздумия» приминительно к пыхе немного режут глаз. Я из-за этого и на статью перешел )
                        0
                        Поправил, чтобы не резало глаз :)
                        +1
                        Возможно стоило в проект внедрить паттерн «Комманда», все строковые сообщения убрать в отдельные темплейты, чтобы не засорять код контроллера и их рендеринг перенести в View-ы, где по сути им и место.
                          0
                          хмм, стоит задуматься, спасибо!
                          0
                          Спасибо за статью, что-то подобное пытаюсь реализовать на Python, может уже кто встречал, чтобы я не городил велосипед?

                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                          Самое читаемое