summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--LICENSE2
-rw-r--r--README4
-rw-r--r--README.md28
-rw-r--r--composer.json11
-rw-r--r--composer.lock18
-rw-r--r--src/lib/Logger.php105
-rwxr-xr-xsrc/ssl_expire_notifier.php73
-rw-r--r--ssl_check.php57
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
diff --git a/LICENSE b/LICENSE
index 5eb8161..754e71e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -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
diff --git a/README b/README
deleted file mode 100644
index 2bc1a11..0000000
--- a/README
+++ /dev/null
@@ -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);
- }
-}