Отслеживание окончания доменов и SSL-сертификатов

При работе с множеством сайтов возникает необходимость в постоянном контроле срока окончания доменов и особенно сертификатов т.к. в основном используются 90-дневненые «Let’s Encrypt», и к тому же на многих хостингах нет автоматического продления.

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

monitoring/
├── config.php
├── cron.php
├── chache.json
├── calendar.php

Далее подробнее о каждом файле:

1

Конфигурационный файл, содержит настройки локали PHP, e-mail для уведомлений и списки проверяемых доменов в виде массивов.

<?php
// Ошибки PHP.
//error_reporting(E_ALL);
//ini_set('display_errors', 1); 

// Локаль.
setlocale(LC_ALL, 'ru_RU.utf8');
date_default_timezone_set('Europe/Moscow');
header('Content-type: text/html; charset=utf-8');
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
mb_http_output('UTF-8');
mb_language('uni');

// E-mail для уведомлений.
$email = 'mail@example.com';

// За сколько отправлять уведомление.
$warn = 259200; // 3 дня 

$domains = array(
	'php.ru',
	'php.su',
	'php.net',
	'habr.com',
	'wikipedia.org',
);

$certificates = array(
	'php.ru',
	'php.su',
	'php.net',
	'habr.com',
	'wikipedia.org',
);
PHP
2

PHP-обработчик, получает даты окончания делегирования доменов через серверы whois и сроки действия SSL-сертификатов, далее сохраняет полученные даты в файл chache.json. В случаи ошибки или приближающейся даты окончания отправит сообщение на почту.

<?php
require_once __DIR__ . '/config.php';

$chache = array();
$mail_text = array();

// Проверка доменов
foreach ($domains as $domain) {
	$date = 0;
	$zone = explode('.', $domain);
	$zone = end($zone);

	switch ($zone) { 
		case 'ru': 
		case 'su': 
		case 'рф': $server = 'whois.tcinet.ru'; break;					
		case 'com':		
		case 'net': $server = 'whois.verisign-grs.com'; break;					
		case 'org': $server = 'whois.pir.org'; break;					
	}

	$socket = fsockopen($server, 43);
	if ($socket) {
		fputs($socket, $domain . PHP_EOL);
		while (!feof($socket)) {
			$res = fgets($socket, 128);
			if (mb_stripos($res, 'paid-till:') !== false) {
				$date = explode('paid-till:', $res);
				$date = strtotime(trim($date[1]));
				break;
			}
			if (mb_stripos($res, 'Registry Expiry Date:') !== false) {
				$date = explode('Registry Expiry Date:', $res);
				$date = strtotime(trim($date[1]));
				break;
			}	
		}
		fclose($socket);
	}
	
	if (!empty($date) && time() + $warn > $date) {
		$mail_text[] = $domain . ' - заканчивается ' . date('d.m.Y H:i', $date);
	} elseif (empty($date)) {
		$mail_text[] = $domain . ' - не удалось получить whois';
	} 

	$chache['domains'][$domain] = $date;
}

// Проверка SSL-сертификатов
foreach ($certificates as $domain) {
	$date = 0;
	$url = 'ssl://' . $domain . ':443';
	$context = stream_context_create(
		array(
			'ssl' => array(
				'capture_peer_cert' => true,
				'verify_peer' => false,
				'verify_peer_name' => false
			)
		)
	);

	$fp = @stream_socket_client($url, $err_no, $err_str, 30, STREAM_CLIENT_CONNECT, $context);
	$cert = @stream_context_get_params($fp);
 
	if (empty($err_no)) {
		$info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']);
		$date = $info['validTo_time_t'];
	}
	
	if (!empty($date) && time() + $warn > $date) {
		$mail_text[] = $domain . ' - заканчивается сертификат ' . date('d.m.Y H:i', $date);
	} elseif (empty($date)) {
		$mail_text[] = $domain . ' - не удалось получить сертификат';
	}
	
	$chache['certificates'][$domain] = $date;
}

// Сохранение в файл.
file_put_contents(__DIR__ . '/chache.json', json_encode($chache));

// Вывод данных в браузер.
echo '<pre>' . print_r($chache, true) . '</pre>';

// Отправка уведомления.
if (!empty($mail_text)) {
	mb_send_mail(
		$email,
		'Мониторинг срока действия доменов и SSL-сертификатов', 
		implode('<br>', $mail_text), 
		"MIME-Version: 1.0\r\nContent-Type: text/html;"
	);
}
PHP

Whois-сервер для других доменных зон можно получить здесь.

Данный скрипт должен запускаться по крону, например раз в день, в 9 утра:

0	9	*	*	*	/usr/local/bin/wget -O - -q "https://example.com/monitoring/cron.php"

Письмо уведомление будет примерно следующего вида:

* Чтобы письма не попадали в спам, лучше сделать отправку писем через SMTP.

3

Скрипт носит чисто информационный характер и выводит календарь с выделенными датами окончания сертификатов и доменов. Выполняется прямым вызовом https://ваш_домен/monitoring/calendar.php.

<?php
require_once __DIR__ . '/config.php';

class Calendar 
{
	/**
	 * Вывод календаря на один месяц.
	 */
	public static function  getMonth($month, $year, $events = array())
	{
		$months = array(
			1  => 'Январь',
			2  => 'Февраль',
			3  => 'Март',
			4  => 'Апрель',
			5  => 'Май',
			6  => 'Июнь',
			7  => 'Июль',
			8  => 'Август',
			9  => 'Сентябрь',
			10 => 'Октябрь',
			11 => 'Ноябрь',
			12 => 'Декабрь'
		);
 
		$month = intval($month);
		$out = '
		<div class="calendar-item">
			<div class="calendar-head">' . $months[$month] . ' ' . $year . '</div>
			<table>
				<tr>
					<th>Пн</th>
					<th>Вт</th>
					<th>Ср</th>
					<th>Чт</th>
					<th>Пт</th>
					<th>Сб</th>
					<th>Вс</th>
				</tr>';
 
		$day_week = date('N', mktime(0, 0, 0, $month, 1, $year));
		$day_week--;
 
		$out.= '<tr>';
 
		for ($x = 0; $x < $day_week; $x++) {
			$out.= '<td></td>';
		}
 
		$days_counter = 0;		
		$days_month = date('t', mktime(0, 0, 0, $month, 1, $year));
	
		for ($day = 1; $day <= $days_month; $day++) {
			if (date('j.n.Y') == $day . '.' . $month . '.' . $year) {
				$class = 'today';
			} elseif (time() > strtotime($day . '.' . $month . '.' . $year)) {
				$class = 'last';
			} else {
				$class = '';
			}
			
			$event_show = false;
			$event_text = array();
			if (!empty($events)) {
				foreach ($events as $date => $text) {
					$date = explode('.', $date);
					if (count($date) == 3) {
						$y = explode(' ', $date[2]);
						if (count($y) == 2) {
							$date[2] = $y[0];
						}

						if ($day == intval($date[0]) && $month == intval($date[1]) && $year == $date[2]) {
							$event_show = true;
							$event_text[] = $text;
						}
					} elseif (count($date) == 2) {
						if ($day == intval($date[0]) && $month == intval($date[1])) {
							$event_show = true;
							$event_text[] = $text;
						}
					} elseif ($day == intval($date[0])) {
						$event_show = true;
						$event_text[] = $text;
					}				
				}
			}
			
			if ($event_show) {
				$out.= '<td class="calendar-day ' . $class . ' event">' . $day;
				if (!empty($event_text)) {
					$out.= '<div class="calendar-popup">' . implode('<br>', $event_text) . '</div>';
					}
				$out.= '</td>';
			} else {
				$out.= '<td class="calendar-day ' . $class . '">' . $day . '</td>';
			}
 
			if ($day_week == 6) {
				$out.= '</tr>';
				if (($days_counter + 1) != $days_month) {
					$out.= '<tr>';
				}
				$day_week = -1;
			}
 
			$day_week++; 
			$days_counter++;
		}
 
		$out .= '</tr></table></div>';
		return $out;
	}
	
	/**
	 * Вывод календаря на несколько месяцев.
	 */
	public static function  getInterval($start, $end, $events = array())
	{
		$curent = explode('.', $start);
		$curent[0] = intval($curent[0]);
		
		$end = explode('.', $end);
		$end[0] = intval($end[0]);
 
		$begin = true;
		$out = '<div class="calendar-wrp">';
		do {
			$out .= self::getMonth($curent[0], $curent[1], $events);
 
			if ($curent[0] == $end[0] && $curent[1] == $end[1]) {
				$begin = false;
			}		
 
			$curent[0]++;
			if ($curent[0] == 13) {
				$curent[0] = 1;
				$curent[1]++;
			}
		} while ($begin == true);	
		
		$out .= '</div>';
		return $out;
	}
}

?><!DOCTYPE html>
<html lang="ru">
<head>
	<meta http-equiv="content-type" content="text/html; charset=utf-8">
	<title>Мониторинг срока действия доменов и SSL-сертификатов</title>
	<style type="text/css">
	body {font: 14px/1.2 Arial, sans-serif;}
	.wrapper {width: 940px;padding: 15px;margin: 0 auto;}
	.errors {margin-bottom: 20px;border: 1px solid red;padding: 15px;background: #fff4f4;color: red;}

	/* Стили календаря */
	.calendar-item {width: 200px;display: inline-block;vertical-align: top;margin: 0 16px 20px;}
	.calendar-head {text-align: center;padding: 5px;font-weight: 700;font-size: 14px;}
	.calendar-item table {border-collapse: collapse;width: 100%;}
	.calendar-item th {font-size: 12px;padding: 6px 7px;text-align: center;color: #888;font-weight: normal;}
	.calendar-item td {font-size: 13px;padding: 6px 5px;text-align: center;border: 1px solid #ddd;}
	.calendar-item tr th:nth-child(6), .calendar-item tr th:nth-child(7),
	.calendar-item tr td:nth-child(6), .calendar-item tr td:nth-child(7)  {color: #e65a5a;}	
	.calendar-day.last {color: #999 !important;}	
	.calendar-day.today {font-weight: bold;}
	.calendar-day.event {background: #ffadad;position: relative;cursor: pointer;}
	.calendar-day.event:hover .calendar-popup {display: block;}
	.calendar-popup {display: none;position: absolute;top: 40px;left: 0;min-width: 200px;padding: 15px;background: #fff;text-align: left;font-size: 13px;z-index: 100;box-shadow: 0 0 10px rgba(0,0,0,0.5);color: #000;}
	.calendar-popup:before {content: ""; border: solid transparent;position: absolute;left: 8px;bottom: 100%;border-bottom-color: #fff;border-width: 9px;margin-left: 0;}
	</style>
</head>
<body>
<div class="wrapper">
	<h1>Мониторинг доменов и SSL-сертификатов</h1>
	
	<?php
	// Загружаем данные из кеша.
	$data = json_decode(file_get_contents(__DIR__ . '/chache.json'), true);
	if (empty($data['domains']) && empty($data['certificates'])) {
		echo '<div class="errors">Нет данных для отображения</div>';
	} else {
		// Формируем данные для календаря
		$events = array();
		$date_end = 0;

		if (!empty($data['domains'])) {
			foreach ($data['domains'] as $i => $row) {
				if (empty($row)) {
					$errors[] = 'Не удалось проверить домен ' . $i;
				} else {
					$events[date('d.m.Y H:i', $row)] = 'Заканчивается домен ' . $i;
					if ($row > $date_end) {
						$date_end = $row;
					}
				}
			}
		}
		
		if (!empty($data['certificates'])) {
			foreach ($data['certificates'] as $i => $row) {
				if (empty($row)) {
					$errors[] = 'Не удалось проверить сертификат ' . $i;
				} else {
					$events[date('d.m.Y H:i', $row)] = 'Заканчивается сертификат ' . $i;
					if ($row > $date_end) {
						$date_end = $row;
					}
				}
			}
		}

		// Вывод ошибок.
		if (!empty($errors)) {
			echo '<div class="errors">' . implode('<br>', $errors) . '</div>';
		}
		
		// Вывод календаря.
		if (!empty($date_end)) {
			echo Calendar::getInterval(date('m.Y'), date('m.Y', $date_end), $events);
		}
	}
	?>	
</div>
</body>
</html>
PHP
4
27.01.2021, обновлено 04.12.2021
8205

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

Юрий Юрий
13 июля 2021 в 10:49
Приветствую.
Для зоны *.kz есть альтернативы whois.nic.kz? С этим не определяет дату.
Igor Himchenko Igor Himchenko
11 августа 2021 в 17:18
Добрый день!
При проверки доменов не возвращает дату!
Notice: Undefined variable: server in /usr/share/zabbix/cron.php on line 22
Warning: fsockopen(): php_network_getaddresses: getaddrinfo failed: Имя или служба не известны in /usr/share/zabbix/cron.php on line 22
Warning: fsockopen(): unable to connect to :43 (php_network_getaddresses: getaddrinfo failed: Имя или служба не известны) in /usr/share/zabbix/cron.php on line 22
---- cron.php -------------------------
switch ($zone) {
case 'com': $server = 'whois.crsnic.net'; break;
case 'net': $server = 'whois.crsnic.net'; break;
case 'org': $server = 'whois.crsnic.net'; break;
case 'com.ua': $server = 'whois.crsnic.net'; break;
case 'gov.ua': $server = 'whois.crsnic.net'; break;
case 'org.ua': $server = 'whois.crsnic.net'; break;
}

$socket = fsockopen($server, 43);
if ($socket) {
fputs($socket, $domain . PHP_EOL);
while (!feof($socket)) {
$res = fgets($socket, 128);
if (mb_stripos($res, 'paid-till:') !== false) {
$date = explode('paid-till:', $res);
$date = strtotime(trim($date[1]));
break;
}
if (mb_stripos($res, 'Registry Expiry Date:') !== false) {
$date = explode('Registry Expiry Date:', $res);
$date = strtotime(trim($date[1]));
break;
}
}
fclose($socket);
}
Николай Ставрогин Николай Ставрогин
19 октября 2021 в 18:28
Все работает нормально.
Но посылается в зашифрованные письма в bas64
Если хотите не шифровать то надо поменять mb_send_mail , на mail

, чтобы добавить комментарий.

Другие публикации

Список серверов Whois
В данной таблице собраны все WHOIS-серверы (43-й порт), которые предоставляют информацию о доменах.
7481
+3
Выполнение заданий по Cron
Cron — UNIX-программа, которая используются для периодического выполнения заданий в определённое время. Расписание и действия описываются инструкциями в файлах crontab, их можно посмотреть через SSH,...
46589
+2
Получение бесплатного SSL-сертификата Let’s Encrypt
Бесплатные 90-дневные сертификаты «Let’s Encrypt» до 2020 года можно было получить в любом количестве на сайте...
7667
+1
Работа с JSON в PHP
JSON (JavaScript Object Notation) – текстовый формат обмена данными, основанный на JavaScript, который представляет собой набор пар {ключ: значение}. Значение может быть массивом, числом, строкой и...
114045
+15
Отправка писем через SMTP в PHPMailer
В последнее время письма отправляемые с хостингов через функции mail() и mb_send_mail() часто попадают в спам или...
138589
+30
Адреса серверов POP3, IMAP и SMTP
Список почтовых серверов популярных хостингов и бесплатных сервисов для настройки почтовых клиентов и скриптов отправки...
77705
+6