diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | LICENSE | 2 | ||||
-rw-r--r-- | README | 4 | ||||
-rw-r--r-- | README.md | 28 | ||||
-rw-r--r-- | composer.json | 11 | ||||
-rw-r--r-- | composer.lock | 18 | ||||
-rw-r--r-- | src/lib/Logger.php | 105 | ||||
-rwxr-xr-x | src/ssl_expire_notifier.php | 73 | ||||
-rw-r--r-- | ssl_check.php | 57 |
9 files changed, 238 insertions, 62 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ab27d1e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +/vendor
\ No newline at end of file @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021 Evgeny Zinoviev +Copyright (c) 2021, 2022 Evgeny Zinoviev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in @@ -1,4 +0,0 @@ -Simple PHP script that checks SSL certificates expiration dates for a list of given domains -and notifies you via Telegram if some of them are about to expire. - -Supposed to be run by cron daily or so. diff --git a/README.md b/README.md new file mode 100644 index 0000000..79dce74 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# ssl_expire_notifier + +Simple PHP script that checks SSL certificates expiration dates for a list of given domains +and notifies you via Telegram if some of them are about to expire. + +Supposed to be run by cron daily or so. + +## Configuration + +Config file is expected to be at `~/.config/ssl_expire_notifier.ini`. + +```ini +telegram_enabled = 1 +telegram_token = "your_bot_token" +telegram_chat_id = "your_chat_id" + +verbose = 1 +warn_days = 60 +error_days = 30 + +hosts[] = example.org +hosts[] = mail.example.com:993 +``` + + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..43804f4 --- /dev/null +++ b/composer.json @@ -0,0 +1,11 @@ +{ + "require": { + "ext-openssl": "*", + "ext-json": "*" + }, + "config": { + "platform": { + "php": "7.4" + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..09efa8a --- /dev/null +++ b/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "8d070178755c320c69f93ee4800660ef", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/src/lib/Logger.php b/src/lib/Logger.php new file mode 100644 index 0000000..bf86133 --- /dev/null +++ b/src/lib/Logger.php @@ -0,0 +1,105 @@ +<?php + +class Logger { + + const DEBUG = 0; + const INFO = 1; + const WARNING = 2; + const ERROR = 3; + const FATAL = 4; + + protected static array $levelColors = [ + self::INFO => 34, + self::WARNING => 33, + self::ERROR => 31, + self::FATAL => 91, + ]; + + protected static array $levelEmojis = [ + self::INFO => 'ℹ️', + self::WARNING => '⚠️', + self::ERROR => '‼️', + self::FATAL => '⚡️' + ]; + + protected string $domain; + + public function __construct(string $domain) { + $this->domain = $domain; + } + + protected function stderr(string $message, $color = null) { + $fmt = "[%s] %s"; + if (is_int($color)) + $fmt = "\033[{$color}m$fmt\033[0m"; + $fmt .= "\n"; + $message = strip_tags($message); + fprintf(STDERR, $fmt, $this->domain, $message); + } + + protected function telegram(string $message) { + global $config; + + $url = 'https://api.telegram.org/bot'.$config['telegram_token'].'/sendMessage'; + $query_content = http_build_query([ + 'chat_id' => $config['telegram_chat_id'], + 'text' => $message, + 'parse_mode' => 'html' + ]); + + $ctx = stream_context_create([ + 'http' => [ + 'header' => [ + 'Content-type: application/x-www-form-urlencoded', + 'Content-Length: '.strlen($query_content) + ], + 'method' => 'POST', + 'content' => $query_content + ] + ]); + + $fp = @fopen($url, 'r', false, $ctx); + if ($fp === false) { + $this->stderr("fopen failed"); + return; + } + + $result = stream_get_contents($fp); + fclose($fp); + + $result = json_decode($result, true); + if (!$result['ok']) + $this->stderr("telegram did not OK"); + } + + protected function report(int $level, string $message) { + global $config; + + if ($config['verbose']) + $this->stderr($message, self::$levelColors[$level] ?? null); + + if ($level != self::DEBUG && ($config['telegram_enabled'] ?? 1) == 1) + $this->telegram(self::$levelEmojis[$level].' '.$this->domain.': '.$message); + } + + public function debug(string $message) { + $this->report(self::DEBUG, $message); + } + + public function info(string $message) { + $this->report(self::INFO, $message); + } + + public function warn(string $message) { + $this->report(self::WARNING, $message); + } + + public function error(string $message) { + $this->report(self::ERROR, $message); + } + + public function fatal(string $message) { + $this->report(self::FATAL, $message); + } + +}
\ No newline at end of file diff --git a/src/ssl_expire_notifier.php b/src/ssl_expire_notifier.php new file mode 100755 index 0000000..e6549e6 --- /dev/null +++ b/src/ssl_expire_notifier.php @@ -0,0 +1,73 @@ +#!/usr/bin/env php +<?php + +require_once __DIR__.'/lib/Logger.php'; + +error_reporting(E_ALL); +ini_set('display_errors', 1); + +$file = getenv('HOME').'/.config/ssl_expire_notifier.ini'; +if (!file_exists($file)) + die('ERROR: config '.$file.' not found'); + +$config = parse_ini_file($file); + +function ssl_expire_notifier() { + global $config; + $now = time(); + + foreach ($config['hosts'] as $host) { + $logger = new Logger($host); + if (($pos = strpos($host, ':')) !== false) { + $port = substr($host, $pos+1); + if (!is_numeric($port)) { + $logger->error("failed to parse host"); + continue; + } + $host = substr($host, 0, $pos); + } else { + $port = 443; + } + + $ipv4 = gethostbyname($host); + if (!$ipv4 || $ipv4 == $host) { + $logger->error("failed to resolve"); + continue; + } + + $logger->debug("resolved to $ipv4"); + + $get = stream_context_create([ + 'ssl' => [ + 'capture_peer_cert' => true, + 'verify_peer' => false, + 'verify_peer_name' => false, + 'allow_self_signed' => true, + 'verify_depth' => 0, + ] + ]); + $read = stream_socket_client('ssl://'.$host.':'.$port, $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $get); + $cert = stream_context_get_params($read); + $cert_info = openssl_x509_parse($cert['options']['ssl']['peer_certificate']); + + $valid_till = $cert_info['validTo_time_t']; + $logger->debug("valid till ".date('d.m.Y, H:i:s', $valid_till)); + + if ($valid_till <= $now) { + $logger->fatal('already expired at '.date('d.m.Y, H:i:s', $valid_till)); + } else { + $method = null; + if ($valid_till-$now < 86400*$config['error_days']) + $method = 'error'; + else if ($valid_till-$now < 86400*$config['warn_days']) + $method = 'warn'; + + if ($method !== null) + call_user_func([$logger, $method], "expires at ".date('d.m.Y, H:i:s', $valid_till)); + else + $logger->debug('ok'); + } + } +} + +ssl_expire_notifier();
\ No newline at end of file diff --git a/ssl_check.php b/ssl_check.php deleted file mode 100644 index 3d60b97..0000000 --- a/ssl_check.php +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env php -<?php - -function notify($text) { - $fields = [ - 'chat_id' => TELEGRAM_CHAT_ID, - 'text' => $text, - ]; - - $ch = curl_init(); - $url = 'https://api.telegram.org/bot'.TELEGRAM_BOT_TOKEN.'/sendMessage'; - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); - curl_setopt($ch, CURLOPT_TIMEOUT, 10); - curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); - curl_exec($ch); - curl_close($ch); -} - -$domains = [ - 'example.com', - 'example.org', - // add domains here -]; -$now = time(); - -const TELEGRAM_CHAT_ID = 0; -const TELEGRAM_BOT_TOKEN = ''; - -foreach ($domains as $d) { - $ipv4 = gethostbyname($d); - if ($ipv4 == $d) { - echo $d.": gethostbyname did not found ipv4\n"; - continue; - } - - $get = stream_context_create([ - 'ssl' => [ - 'capture_peer_cert' => true, - 'verify_peer' => false, - 'verify_peer_name' => false, - 'allow_self_signed' => true, - 'verify_depth' => 0, - ] - ]); - $read = stream_socket_client('ssl://'.$d.':443', $errno, $errstr, 30, STREAM_CLIENT_CONNECT, $get); - $cert = stream_context_get_params($read); - $certinfo = openssl_x509_parse($cert['options']['ssl']['peer_certificate']); - - $valid_to = $certinfo['validTo_time_t']; - if ($valid_to - $now < 86400*7) { - $text = "SSL-сертификат для {$d} истекает ".date('d.m.Y H:i:s', $valid_to); - notify($text); - } -} |