diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..56a108b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/check_ssl_cert"] + path = lib/check_ssl_cert + url = https://github.com/matteocorti/check_ssl_cert.git diff --git a/README.md b/README.md index 984ba13..02ce44b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,27 @@ Domain expiration ================= -Checks the expiration dates of domains. +Checks the expiration dates of domains et HTTPS certificates. ## Installation and usage -PHP 7.3, [composer](https://getcomposer.org/) and `whois` required. +PHP 7.3, [composer](https://getcomposer.org/), `whois` and `openssl` required. ```text -$ git clone https://gitnet.fr/deblan/domain-expiration.git +$ git clone --recurse-submodules https://gitnet.fr/deblan/domain-expiration.git $ cd domain-expiration $ composer install ``` ```text -$ php7.3 ./domain-expiration check example.com other-example.com +$ php7.3 ./check domains example.com other-example.com ++-------------------+------+---------------------+ +| Domain | Days | Date | ++-------------------+------+---------------------+ +| example.com | XX | YYYY-MM-DD HH:MM:SS | +| other-example.com | XXX | YYYY-MM-DD HH:MM:SS | ++-------------------+------+---------------------+ +$ php7.3 ./check https-certificates example.com other-example.com +-------------------+------+---------------------+ | Domain | Days | Date | +-------------------+------+---------------------+ diff --git a/check b/check new file mode 100755 index 0000000..d57ea46 --- /dev/null +++ b/check @@ -0,0 +1,15 @@ +#!/usr/bin/env php7.3 +add(new CheckDomainsCommand()); +$application->add(new CheckHttpsCertificatesCommand()); +$application->run(); diff --git a/domain-expiration b/domain-expiration deleted file mode 100755 index dadb1b7..0000000 --- a/domain-expiration +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env php7.3 -add(new CheckCommand()); -$application->run(); diff --git a/lib/check_ssl_cert b/lib/check_ssl_cert new file mode 160000 index 0000000..0af396b --- /dev/null +++ b/lib/check_ssl_cert @@ -0,0 +1 @@ +Subproject commit 0af396b41a5dc159e31689812bdbdd0203394cfd diff --git a/src/Deblan/Command/CheckCommand.php b/src/Deblan/Command/CheckDomainsCommand.php similarity index 98% rename from src/Deblan/Command/CheckCommand.php rename to src/Deblan/Command/CheckDomainsCommand.php index 9b915b8..2cf4b9e 100644 --- a/src/Deblan/Command/CheckCommand.php +++ b/src/Deblan/Command/CheckDomainsCommand.php @@ -12,11 +12,11 @@ use Deblan\Whois\Parser; use Symfony\Component\Console\Input\InputOption; /** - * class CheckCommand. + * class CheckDomainsCommand. * * @author Simon Vieille */ -class CheckCommand extends BaseCommand +class CheckDomainsCommand extends BaseCommand { /** * @var array @@ -44,7 +44,7 @@ class CheckCommand extends BaseCommand protected function configure() { $this - ->setName('check') + ->setName('domains') ->addArgument('domains', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of domains') ->addOption('short', 's', InputOption::VALUE_NONE, 'Simplify the table output') ->addOption('no-headers', null, InputOption::VALUE_NONE, 'Remove the table headers') diff --git a/src/Deblan/Command/CheckHttpsCertificatesCommand.php b/src/Deblan/Command/CheckHttpsCertificatesCommand.php new file mode 100644 index 0000000..2dbcf0b --- /dev/null +++ b/src/Deblan/Command/CheckHttpsCertificatesCommand.php @@ -0,0 +1,269 @@ + + */ +class CheckHttpsCertificatesCommand extends BaseCommand +{ + /** + * @var array + */ + protected $successes = []; + + /** + * @var array + */ + protected $fails = []; + + /** + * @var InputInterface + */ + protected $input; + + /** + * @var OutputInterface + */ + protected $output; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setName('https-certificates') + ->addArgument('domains', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of domains') + ->addOption('short', 's', InputOption::VALUE_NONE, 'Simplify the table output') + ->addOption('no-headers', null, InputOption::VALUE_NONE, 'Remove the table headers') + ->addOption('wait', 'w', InputOption::VALUE_REQUIRED, 'Wait between each whois (in second, default: 0)') + ->addOption('json', 'j', InputOption::VALUE_NONE, 'Select json as output') + ->addOption('table', 't', InputOption::VALUE_NONE, 'Select table as output (default)') + ->setHelp(<<<'EOF' +The %command.name% retrieves the expiration dates of the given domains. + +Example: %command.full_name% example.com other-example.com +EOF +); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + $wait = (int) $this->input->getOption('wait'); + + $this->checkHttpsCertificates($wait); + + $short = $this->input->getOption('short'); + $json = $this->input->getOption('json'); + $table = $this->input->getOption('table') || !$json; + $noHeaders = $this->input->getOption('no-headers'); + + $successes = $this->sort($this->successes); + $fails = $this->sort($this->fails); + + if ($json) { + return $this->output->write(json_encode(array_merge($successes, $fails))); + } + + if ($table) { + $this->renderTable($successes, $fails, $short, $noHeaders); + } + } + + /** + * Render a Json. + * + * @param array $successes + * @param array $fails + */ + protected function renderJson(array $successes, array $fails):void + { + $data = array_merge($successes, $fails); + $json = json_encode($data); + + $this->output->write($json); + } + + /** + * Renders a Table. + * + * @param array $successes + * @param array $fails + * @param bool $short + * @param bool $noHeader + */ + protected function renderTable(array $successes, array $fails, bool $short, bool $noHeaders):void + { + $table = new Table($this->output); + + if (!$noHeaders) { + $table->setHeaders(['Domain', 'Days', 'Date']); + } + + if ($short) { + $table->setStyle('compact'); + } + + foreach ($successes as $result) { + $table->addRow([ + $result['domain'], + $result['dayUntilExpiry'], + $this->createDateRender($result['expiryDate']), + ]); + } + + foreach ($fails as $result) { + $table->addRow([ + $result['domain'], + $result['dayUntilExpiry'], + 'FAIL', + ]); + } + + $table->render(); + } + + /** + * Extracts domains. + * + * @return array + */ + protected function getDomains():array + { + $inputDomains = $this->input->getArgument('domains'); + $domains = []; + + foreach ($inputDomains as $inputDomain) { + $value = explode(',', $inputDomain); + $value = array_map('trim', $value); + + $domains = array_merge($value, $domains); + } + + return $domains; + } + + /** + * Checks domains. + */ + protected function checkHttpsCertificates(int $wait):void + { + $domains = $this->getDomains(); + $count = count($domains); + + foreach ($domains as $key => $domain) { + $data = $this->checkHttpsCertificate($domain); + + if ($data['expiryDate'] === null) { + $this->fails[] = $data; + } else { + $this->successes[] = $data; + } + + if ($wait > 0 && $key !== $count - 1) { + sleep($wait); + } + } + } + + /** + * Checks domain. + * + * @return array + */ + protected function checkHttpsCertificate($domain):array + { + $process = new Process([ + 'lib/check_ssl_cert/check_ssl_cert', + '-H', + $domain, + ]); + $process->run(); + + $whois = $process->getOutput(); + $parser = new Parser($whois); + $expiryDate = $parser->getExpiryDate(); + + if ($expiryDate) { + $comparison = $expiryDate->getTimestamp(); + $dayUntilExpiry = floor(($expiryDate->getTimestamp() - time()) / 3600 / 24); + } else { + $comparison = 'FAIL'; + $dayUntilExpiry = null; + } + + return [ + 'domain' => $domain, + 'expiryDate' => $expiryDate, + 'dayUntilExpiry' => $dayUntilExpiry, + 'comparison' => $expiryDate ? $expiryDate->getTimestamp() : 'FAIL', + ]; + } + + /** + * Sorts by expiry date and domain. + * + * @param array $data + * + * @return array + */ + protected function sort(array $data):array + { + usort($data, function ($a, $b) { + if ($a['comparison'] > $b['comparison']) { + return 1; + } + + if ($a['comparison'] === $b['comparison']) { + if ($a['domain'] > $b['domain']) { + return 1; + } + + return -1; + } + + return 0; + }); + + return $data; + } + + /** + * Creates date render for the table. + * + * @param \DateTime $date + * + * @return string + */ + protected function createDateRender(\DateTime $date):string + { + $timestamp = $date->getTimestamp(); + + if ($timestamp - time() < 3600 * 24 * 14) { + $color = 'red'; + } elseif ($timestamp - time() < 3600 * 24 * 30) { + $color = 'yellow'; + } else { + $color = 'green'; + } + + return sprintf('%s', $color, $date->format('Y-m-d H:i:s')); + } +} diff --git a/src/Deblan/SslCert/Parser.php b/src/Deblan/SslCert/Parser.php new file mode 100644 index 0000000..9719a7c --- /dev/null +++ b/src/Deblan/SslCert/Parser.php @@ -0,0 +1,53 @@ + + */ +class Parser +{ + /** + * @var string + */ + protected $check; + + /** + * Constructor. + * + * @param mixed $whois + */ + public function __construct(string $check) + { + $this->check = $check; + } + + /** + * Extracts expiry date. + * + * @return DateTime|null + */ + public function getExpiryDate(): ? \DateTime + { + preg_match('/days=([^;]+);/', $this->check, $match); + + if (isset($match[1])) { + $days = (int) $match[1]; + + if ($days > 0) { + $date = 'now +'.$days.' days'; + } else { + $date = 'now '.$days.' days'; + } + + try { + return new \DateTime($date); + } catch (\Exception $e) { + } + } + + return null; + } +}