rewrite script, use config file instead of hardcoded domains list
This commit is contained in:
parent
47bef947d8
commit
22cd7549ea
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/.idea
|
||||||
|
/vendor
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
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
|
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
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
4
README
4
README
@ -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
28
README.md
Normal 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
11
composer.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"require": {
|
||||||
|
"ext-openssl": "*",
|
||||||
|
"ext-json": "*"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"platform": {
|
||||||
|
"php": "7.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
18
composer.lock
generated
Normal file
18
composer.lock
generated
Normal 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
105
src/lib/Logger.php
Normal 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
73
src/ssl_expire_notifier.php
Executable 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();
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user