rewrite script, use config file instead of hardcoded domains list

This commit is contained in:
Evgeny Zinoviev 2022-09-22 00:24:10 +03:00
parent 47bef947d8
commit 22cd7549ea
9 changed files with 238 additions and 62 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.idea
/vendor

View 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

4
README
View File

@ -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.

28
README.md Normal file
View File

@ -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

11
composer.json Normal file
View File

@ -0,0 +1,11 @@
{
"require": {
"ext-openssl": "*",
"ext-json": "*"
},
"config": {
"platform": {
"php": "7.4"
}
}
}

18
composer.lock generated Normal file
View File

@ -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"
}

105
src/lib/Logger.php Normal file
View File

@ -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);
}
}

73
src/ssl_expire_notifier.php Executable file
View File

@ -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();

View File

@ -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);
}
}