From 45d1c03e5a15679295baca63c0bdf30067b1b723 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 4 Feb 2020 16:44:48 +0100 Subject: [PATCH] Feature/ssl cert expiration check (#831) Added ssl expiration to warning state. --- src/includes/functions.inc.php | 7 ++- src/includes/psmconfig.inc.php | 2 +- src/lang/en_US.lang.php | 7 +++ .../Controller/AbstractServerController.php | 13 ++++- .../Server/Controller/ServerController.php | 5 ++ .../Server/Controller/StatusController.php | 2 + src/psm/Util/Install/Installer.php | 18 +++++++ src/psm/Util/Server/ServerValidator.php | 14 +++++ src/psm/Util/Server/Updater/StatusUpdater.php | 52 ++++++++++++++++--- .../module/server/server/update.tpl.html | 4 +- .../module/server/server/view.tpl.html | 6 +-- 11 files changed, 116 insertions(+), 14 deletions(-) diff --git a/src/includes/functions.inc.php b/src/includes/functions.inc.php index 85d3cb63..3036c8fa 100644 --- a/src/includes/functions.inc.php +++ b/src/includes/functions.inc.php @@ -390,7 +390,7 @@ namespace { * @param string|bool $website_password Password website * @param string|null $request_method Request method like GET, POST etc. * @param string|null $post_field POST data - * @return string cURL result + * @return array cURL result */ function psm_curl_get( $href, @@ -418,6 +418,7 @@ namespace { curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_ENCODING, ''); + curl_setopt($ch, CURLOPT_CERTINFO, 1); if (!empty($request_method)) { curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request_method); @@ -455,7 +456,9 @@ namespace { PSM_VERSION . '; +https://github.com/phpservermon/phpservermon)'); } - $result = curl_exec($ch); + $result['exec'] = curl_exec($ch); + $result['info'] = curl_getinfo($ch); + curl_close($ch); if (defined('PSM_DEBUG') && PSM_DEBUG === true && psm_is_cli()) { diff --git a/src/includes/psmconfig.inc.php b/src/includes/psmconfig.inc.php index 530753c8..314eccdb 100644 --- a/src/includes/psmconfig.inc.php +++ b/src/includes/psmconfig.inc.php @@ -30,7 +30,7 @@ /** * Current PSM version */ -define('PSM_VERSION', '3.4.5'); +define('PSM_VERSION', '3.4.6-beta.1'); /** * URL to check for updates. Will not be checked if turned off on config page. diff --git a/src/lang/en_US.lang.php b/src/lang/en_US.lang.php index 621a6aed..570d32c2 100644 --- a/src/lang/en_US.lang.php +++ b/src/lang/en_US.lang.php @@ -247,6 +247,11 @@ $sm_lang = array( 'hour' => 'Hour', 'warning_threshold' => 'Warning threshold', 'warning_threshold_description' => 'Number of failed checks required before it is marked offline.', + 'ssl_cert_expiry_days' => 'SSL Certificate Validity', + 'ssl_cert_expiry_days_description' => 'The minimum remaining days the SSL certificate is still valid. Use 0 to + disable check.', + 'ssl_cert_expired' => 'SSL certificate expired since', + 'ssl_cert_expiring' => 'SSL certificate expiring:', 'chart_last_week' => 'Last week', 'chart_history' => 'History', 'chart_day_format' => '%Y-%m-%d', @@ -264,6 +269,8 @@ $sm_lang = array( 'error_server_ip_bad_website' => 'The website URL is not valid.', 'error_server_type_invalid' => 'The selected server type is invalid.', 'error_server_warning_threshold_invalid' => 'The warning threshold must be a valid integer greater than 0.', + 'error_server_ssl_cert_expiry_days' => 'The remaining days for SSL certificate validity must be a valid integer + greater than or equal to 0.', ), 'config' => array( 'general' => 'General', diff --git a/src/psm/Module/Server/Controller/AbstractServerController.php b/src/psm/Module/Server/Controller/AbstractServerController.php index 1a1c210e..efc70072 100644 --- a/src/psm/Module/Server/Controller/AbstractServerController.php +++ b/src/psm/Module/Server/Controller/AbstractServerController.php @@ -85,6 +85,8 @@ abstract class AbstractServerController extends AbstractController `s`.`telegram`, `s`.`warning_threshold`, `s`.`warning_threshold_counter`, + `s`.`ssl_cert_expiry_days`, + `s`.`ssl_cert_expired_time`, `s`.`timeout`, `s`.`website_username`, `s`.`website_password`, @@ -120,7 +122,16 @@ abstract class AbstractServerController extends AbstractController } $server['last_check'] = psm_timespan($server['last_check']); - if ($server['status'] == 'on' && $server['warning_threshold_counter'] > 0) { + if ( + ( + $server['status'] == 'on' && + $server['warning_threshold_counter'] > 0 + ) || ( + $server['status'] == 'on' && + $server['ssl_cert_expired_time'] !== null && + $server['ssl_cert_expiry_days'] > 0 + ) + ) { $server['status'] = 'warning'; } diff --git a/src/psm/Module/Server/Controller/ServerController.php b/src/psm/Module/Server/Controller/ServerController.php index 097329fd..ab27dd68 100644 --- a/src/psm/Module/Server/Controller/ServerController.php +++ b/src/psm/Module/Server/Controller/ServerController.php @@ -208,6 +208,7 @@ class ServerController extends AbstractServerController 'edit_value_website_username' => $edit_server['website_username'], 'edit_value_website_password' => empty($edit_server['website_password']) ? '' : sha1($edit_server['website_password']), + 'edit_value_ssl_cert_expiry_days' => $edit_server['ssl_cert_expiry_days'], 'edit_type_selected_' . $edit_server['type'] => 'selected="selected"', 'edit_active_selected' => $edit_server['active'], 'edit_email_selected' => $edit_server['email'], @@ -281,6 +282,7 @@ class ServerController extends AbstractServerController 'header_name' => psm_POST('header_name', ''), 'header_value' => psm_POST('header_value', ''), 'warning_threshold' => intval(psm_POST('warning_threshold', 0)), + 'ssl_cert_expiry_days' => intval(psm_POST('ssl_cert_expiry_days', 1)), 'active' => in_array($_POST['active'], array('yes', 'no')) ? $_POST['active'] : 'no', 'email' => in_array($_POST['email'], array('yes', 'no')) ? $_POST['email'] : 'no', 'sms' => in_array($_POST['sms'], array('yes', 'no')) ? $_POST['sms'] : 'no', @@ -325,6 +327,7 @@ class ServerController extends AbstractServerController $server_validator->type($clean['type']); $server_validator->ip($clean['ip'], $clean['type']); $server_validator->warningThreshold($clean['warning_threshold']); + $server_validator->sslCertExpiryDays($clean['ssl_cert_expiry_days']); } catch (\InvalidArgumentException $ex) { $this->addMessage(psm_get_lang('servers', 'error_' . $ex->getMessage()), 'error'); return $this->executeEdit(); @@ -558,6 +561,8 @@ class ServerController extends AbstractServerController 'label_users' => psm_get_lang('servers', 'users'), 'label_warning_threshold' => psm_get_lang('servers', 'warning_threshold'), 'label_warning_threshold_description' => psm_get_lang('servers', 'warning_threshold_description'), + 'label_ssl_cert_expiry_days' => psm_get_lang('servers', 'ssl_cert_expiry_days'), + 'label_ssl_cert_expiry_days_description' => psm_get_lang('servers', 'ssl_cert_expiry_days_description'), 'label_action' => psm_get_lang('system', 'action'), 'label_save' => psm_get_lang('system', 'save'), 'label_go_back' => psm_get_lang('system', 'go_back'), diff --git a/src/psm/Module/Server/Controller/StatusController.php b/src/psm/Module/Server/Controller/StatusController.php index efe840f3..d4ee083d 100644 --- a/src/psm/Module/Server/Controller/StatusController.php +++ b/src/psm/Module/Server/Controller/StatusController.php @@ -100,6 +100,8 @@ class StatusController extends AbstractServerController $layout_data['servers_offline'][] = $server; } elseif ($server['warning_threshold_counter'] > 0) { $layout_data['servers_warning'][] = $server; + } elseif ($server['ssl_cert_expired_time'] !== null && $server['ssl_cert_expiry_days'] > 0) { + $layout_data['servers_warning'][] = $server; } else { $layout_data['servers_online'][] = $server; } diff --git a/src/psm/Util/Install/Installer.php b/src/psm/Util/Install/Installer.php index eb307294..78f078c9 100644 --- a/src/psm/Util/Install/Installer.php +++ b/src/psm/Util/Install/Installer.php @@ -265,6 +265,8 @@ class Installer `telegram` enum('yes','no') NOT NULL default 'yes', `warning_threshold` mediumint(1) unsigned NOT NULL DEFAULT '1', `warning_threshold_counter` mediumint(1) unsigned NOT NULL DEFAULT '0', + `ssl_cert_expiry_days` mediumint(1) unsigned NOT NULL DEFAULT '0', + `ssl_cert_expired_time` varchar(255) NULL, `timeout` smallint(1) unsigned NULL DEFAULT NULL, `website_username` varchar(255) DEFAULT NULL, `website_password` varchar(255) DEFAULT NULL, @@ -343,6 +345,9 @@ class Installer if (version_compare($version_from, '3.4.2', '<')) { $this->upgrade342(); } + if (version_compare($version_from, '3.4.6-beta.1', '<')) { + $this->upgrade346(); + } psm_update_conf('version', $version_to); } @@ -657,4 +662,17 @@ class Installer $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "servers` CHANGE `last_output` `last_output` TEXT;"; $this->execSQL($queries); } + + /** + * Upgrade for v3.4.6 release + */ + protected function upgrade346() + { + $queries = array(); + $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "servers` + ADD `ssl_cert_expiry_days` MEDIUMINT(1) UNSIGNED NOT NULL DEFAULT '0' AFTER `warning_threshold_counter`"; + $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "servers` + ADD `ssl_cert_expired_time` VARCHAR(255) NULL AFTER `ssl_cert_expiry_days`"; + $this->execSQL($queries); + } } diff --git a/src/psm/Util/Server/ServerValidator.php b/src/psm/Util/Server/ServerValidator.php index 63e5688b..18dc658b 100644 --- a/src/psm/Util/Server/ServerValidator.php +++ b/src/psm/Util/Server/ServerValidator.php @@ -154,4 +154,18 @@ class ServerValidator } return true; } + + /** + * Check SSL expiry days + * @param int $value + * @return boolean + * @throws \InvalidArgumentException + */ + public function sslCertExpiryDays($value) + { + if (!is_numeric($value) || $value < 0) { + throw new \InvalidArgumentException('server_ssl_cert_expiry_days'); + } + return true; + } } diff --git a/src/psm/Util/Server/Updater/StatusUpdater.php b/src/psm/Util/Server/Updater/StatusUpdater.php index 8dfa421f..da0cb72e 100644 --- a/src/psm/Util/Server/Updater/StatusUpdater.php +++ b/src/psm/Util/Server/Updater/StatusUpdater.php @@ -41,6 +41,8 @@ class StatusUpdater public $header = ''; + public $curl_info = ''; + public $rtime = 0; public $status_new = false; @@ -86,6 +88,7 @@ class StatusUpdater $this->server_id = $server_id; $this->error = ''; $this->header = ''; + $this->curl_info = ''; $this->rtime = ''; // get server info from db @@ -96,7 +99,7 @@ class StatusUpdater 'type', 'pattern', 'pattern_online', 'post_field', 'allow_http_status', 'redirect_check', 'header_name', 'header_value', 'status', 'active', 'warning_threshold', - 'warning_threshold_counter', 'timeout', 'website_username', + 'warning_threshold_counter', 'ssl_cert_expiry_days', 'ssl_cert_expired_time', 'timeout', 'website_username', 'website_password', 'last_offline' )); if (empty($this->server)) { @@ -263,12 +266,13 @@ class StatusUpdater $this->server['request_method'], $this->server['post_field'] ); - $this->header = $curl_result; + $this->header = $curl_result['exec']; + $this->curl_info = $curl_result['info']; $this->rtime = (microtime(true) - $starttime); // the first line would be the status code.. - $status_code = strtok($curl_result, "\r\n"); + $status_code = strtok($curl_result['exec'], "\r\n"); // keep it general // $code[2][0] = status code // $code[3][0] = name of status code @@ -299,7 +303,7 @@ class StatusUpdater ($this->server['pattern_online'] == 'yes') == !preg_match( "/{$this->server['pattern']}/i", - $curl_result + $curl_result['exec'] ) ) { $this->error = "TEXT ERROR : Pattern '{$this->server['pattern']}' " . @@ -314,7 +318,7 @@ class StatusUpdater $location_matches = array(); preg_match( '/([Ll]ocation: )(https*:\/\/)(www.)?([a-zA-Z.:0-9]*)([\/][[:alnum:][:punct:]]*)/', - $curl_result, + $curl_result['exec'], $location_matches ); if (!empty($location_matches)) { @@ -335,7 +339,7 @@ class StatusUpdater if ($this->server['header_name'] != '' && $this->server['header_value'] != '') { $header_flag = false; // Only get the header text if the result also includes the body - $header_text = substr($curl_result, 0, strpos($curl_result, "\r\n\r\n")); + $header_text = substr($curl_result['exec'], 0, strpos($curl_result['exec'], "\r\n\r\n")); foreach (explode("\r\n", $header_text) as $i => $line) { if ($i === 0 || strpos($line, ':') == false) { continue; // We skip the status code & other non-header lines. Needed for proxy or redirects @@ -362,6 +366,9 @@ class StatusUpdater } } + // Check ssl cert + $this->checkSsl($this->server, $this->error, $result); + // check if server is available and rerun if asked. if (!$result && $run < $max_runs) { return $this->updateWebsite($max_runs, $run + 1); @@ -389,4 +396,37 @@ class StatusUpdater { return $this->rtime; } + + /** + * Check if a server speaks SSL and if the certificate is not expired. + * @param string $error + * @param bool $result + */ + private function checkSsl($server, &$error, &$result) + { + if (version_compare(PHP_RELEASE_VERSION, '7.1', '<')) { + $error = "The server you're running PSM on must use PHP 7.1 or higher to test the SSL expiration."; + return; + } + if ( + !empty($this->curl_info['certinfo']) && + $server['ssl_cert_expiry_days'] > 0 + ) { + $cert_expiration_date = strtotime($this->curl_info['certinfo'][0]['Expire date']); + $expiration_time = round((int)($cert_expiration_date - time()) / 86400); + $latest_time = time() + (86400 * $server['ssl_cert_expiry_days']); + if ($expiration_time >= 0) { + $this->header = psm_get_lang('servers', 'ssl_cert_expiring') . " " . + psm_date($this->curl_info['certinfo'][0]['Expire date']) . + "\n\n" . $this->header; + $save['ssl_cert_expired_time'] = null; + } else { + $error = psm_get_lang('servers', 'ssl_cert_expired') . " " . + psm_timespan($cert_expiration_date) . ".\n\n" . + $error; + $save['ssl_cert_expired_time'] = $expiration_time; + } + $this->db->save(PSM_DB_PREFIX . 'servers', $save, array('server_id' => $this->server_id)); + } + } } diff --git a/src/templates/default/module/server/server/update.tpl.html b/src/templates/default/module/server/server/update.tpl.html index 5d9eee53..e5fb7fbf 100644 --- a/src/templates/default/module/server/server/update.tpl.html +++ b/src/templates/default/module/server/server/update.tpl.html @@ -46,7 +46,9 @@ - {{ macro.input_field("number", "port", "port types typeService", "port", label_custom_port, edit_value_port, null, "5") }} + {{ macro.input_field("number", "port", "port types typeService", "port", label_custom_port, edit_value_port, null, "5") }} + + {{ macro.input_field("number", "ssl_cert_expiry_days", "types typeWebsite", "ssl_cert_expiry_days", label_ssl_cert_expiry_days, edit_value_ssl_cert_expiry_days, 0, "5", 'ssl_cert_help', label_ssl_cert_expiry_days_description) }}
diff --git a/src/templates/default/module/server/server/view.tpl.html b/src/templates/default/module/server/server/view.tpl.html index ff8783a6..05b9c984 100644 --- a/src/templates/default/module/server/server/view.tpl.html +++ b/src/templates/default/module/server/server/view.tpl.html @@ -227,13 +227,13 @@
  • {{ label_last_error }}:
    -
    {{ last_error }}
    +
    {{ last_error|nl2br }}
  • {{ label_last_output }}:
    -
    {{ last_output_truncated }}
    +
    {{ last_output_truncated|nl2br }}
    {% if last_output_truncated != last_output %}
    @@ -247,7 +247,7 @@
  • {{ label_last_error_output }}:
    -
    {{ last_error_output_truncated }}
    +
    {{ last_error_output_truncated|nl2br }}
    {% if last_error_output_truncated != last_error_output %}