diff --git a/public_html/appmonitor/appmonitor-checks.class.php b/public_html/appmonitor/classes/appmonitor-checks.class.php similarity index 79% rename from public_html/appmonitor/appmonitor-checks.class.php rename to public_html/appmonitor/classes/appmonitor-checks.class.php index 198a4ded3a3bde05894e53979584a3b7da2c025c..9301efe3a8b0d4012544623bd48abd821a633726 100644 --- a/public_html/appmonitor/appmonitor-checks.class.php +++ b/public_html/appmonitor/classes/appmonitor-checks.class.php @@ -4,7 +4,8 @@ define("RESULT_OK", 0); define("RESULT_UNKNOWN", 1); define("RESULT_WARNING", 2); define("RESULT_ERROR", 3); -/** + +/** * APPMONITOR CLIENT CHECKS<br> * <br> * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br> @@ -19,8 +20,9 @@ define("RESULT_ERROR", 3); * --------------------------------------------------------------------------------<br> * <br> * --- HISTORY:<br> - * 2014-10-24 0.5 axel.hahn@iml.unibe.ch<br> - * 2015-04-08 0.9 axel.hahn@iml.unibe.ch added sochket test: checkPortTcp<br> + * 2014-10-24 0.5 axel.hahn@iml.unibe.ch<br> + * 2015-04-08 0.9 axel.hahn@iml.unibe.ch added sochket test: checkPortTcp<br> + * 2018-06-29 0.24 axel.hahn@iml.unibe.ch add file and directory checks<br> * --------------------------------------------------------------------------------<br> * @version 0.9 * @author Axel Hahn @@ -30,7 +32,6 @@ define("RESULT_ERROR", 3); * @package IML-Appmonitor */ class appmonitorcheck { - // ---------------------------------------------------------------------- // CONFIG // ---------------------------------------------------------------------- @@ -110,9 +111,11 @@ class appmonitorcheck { private function _checkArrayKeys($aConfig, $sKeyList) { foreach (explode(",", $sKeyList) as $sKey) { if (!array_key_exists($sKey, $aConfig)) { + header('HTTP/1.0 503 Service Unavailable'); die('ERROR in ' . __CLASS__ . "<br>array requires the keys [$sKeyList] - but key '$sKey' was not found in config array <pre>" . print_r($aConfig, true)); } if (is_null($aConfig[$sKey])) { + header('HTTP/1.0 503 Service Unavailable'); die('ERROR in ' . __CLASS__ . "<br> key '$sKey' is empty in config array <pre>" . print_r($aConfig, true)); } } @@ -140,6 +143,7 @@ class appmonitorcheck { * @return array */ public function makeCheck($aConfig) { + $this->_iStart = microtime(true); $this->_checkArrayKeys($aConfig, "name,description,check"); $this->_checkArrayKeys($aConfig["check"], "function"); @@ -148,6 +152,7 @@ class appmonitorcheck { $sCheck = "check" . $this->_aConfig["check"]["function"]; if (!method_exists($this, $sCheck)) { + header('HTTP/1.0 503 Service Unavailable'); die(__CLASS__ . " check not found: $sCheck <pre>" . print_r($aConfig, true)); } $aParams = array_key_exists("params", $this->_aConfig["check"]) ? $this->_aConfig["check"]["params"] : array(); @@ -155,6 +160,7 @@ class appmonitorcheck { // call the check ... call_user_func(array($this, $sCheck), $aParams); + $this->_aData['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; // echo "<pre>"; print_r($this->listChecks()); die(); // ... and send response return $this->respond(); @@ -190,16 +196,47 @@ class appmonitorcheck { // ---------------------------------------------------------------------- /** - * most simple check: set values - * @return type + * check a file + * @param array $aParams + * array( + * "filename" directory that must exist + * "writable" flag to check that it must be writable too + * ) + * @return boolean */ - private function checkSimple($aParams) { - $aHelp = array( - "result" => "(integer) result value", - "value" => "(string) explaination" - ); - $this->_checkArrayKeys($aParams, "result,value"); - return $this->_setReturn((int) $aParams["result"], $aParams["value"]); + public function checkFile($aParams) { + $aOK=array(); + $aErrors=array(); + $this->_checkArrayKeys($aParams, "filename"); + $sFile=$aParams["filename"]; + + if (isset($aParams['exists'])){ + $sMyflag='exists='.($aParams['exists'] ? 'yes' : 'no'); + if (file_exists($sFile) && $aParams['exists']){ + $aOK[]=$sMyflag; + } else { + $aErrors[]=$sMyflag; + } + } + foreach(array('dir', 'executable', 'file', 'link', 'readable', 'writable') as $sFiletest){ + if (isset($aParams[$sFiletest])){ + $sTestCmd='return is_'.$sFiletest.'("'.$sFile.'");'; + if (eval($sTestCmd) && $aParams[$sFiletest]){ + $aOK[]=$sFiletest . '='.($aParams[$sFiletest] ? 'yes' : 'no'); + } else { + $aErrors[]=$sFiletest . '='.($aParams[$sFiletest] ? 'yes' : 'no'); + } + } + } + $sMessage=(count($aOK) ? ' flags OK: ' .implode('|', $aOK) : '') + .' '. (count($aErrors) ? ' flags FAILED: '.implode('|', $aErrors) : '') + ; + if(count($aErrors)){ + $this->_setReturn(RESULT_ERROR, 'file test ['. $sFile . '] '.$sMessage); + } else { + $this->_setReturn(RESULT_OK, 'file test ['. $sFile . '] '.$sMessage); + } + return true; } /** @@ -209,10 +246,12 @@ class appmonitorcheck { * "url" url to fetch * "contains" string that must exist in response body * ) + * @param integer $iTimeout value in sec; default: 5sec */ private function checkHttpContent($aParams, $iTimeout = 5) { $this->_checkArrayKeys($aParams, "url,contains"); if (!function_exists("curl_init")) { + header('HTTP/1.0 503 Service Unavailable'); die("ERROR: PHP CURL module is not installed."); } $ch = curl_init($aParams["url"]); @@ -258,33 +297,6 @@ class appmonitorcheck { } } - /** - * check sqlite connection - * @param array $aParams - * array( - * "db" - * ) - * @return boolean - */ - private function checkSqliteConnect($aParams) { - $this->_checkArrayKeys($aParams, "db"); - if (!file_exists($aParams["db"])) { - $this->_setReturn(RESULT_ERROR, "ERROR: Sqlite database file " . $aParams["db"] . " does not exist."); - return false; - } - try { - // $db = new SQLite3($sqliteDB); - // $db = new PDO("sqlite:".$sqliteDB); - $o = new PDO("sqlite:" . $aParams["db"]); - $this->_setReturn(RESULT_OK, "OK: Sqlite database " . $aParams["db"] . " was connected"); - return true; - } catch (Exception $exc) { - $this->_setReturn(RESULT_ERROR, "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . mysqli_connect_error()); - return false; - } - } - - /** * check if system is listening to a given port * @param array $aParams @@ -296,18 +308,18 @@ class appmonitorcheck { */ private function checkPortTcp($aParams) { $this->_checkArrayKeys($aParams, "port"); - - $sHost=array_key_exists('host',$aParams)?$aParams['host']:'127.0.0.1'; - $iPort=(int)$aParams['port']; - + + $sHost = array_key_exists('host', $aParams) ? $aParams['host'] : '127.0.0.1'; + $iPort = (int) $aParams['port']; + // from http://php.net/manual/de/sockets.examples.php - + $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if ($socket === false) { $this->_setReturn(RESULT_UNKNOWN, "ERROR: $sHost:$iPort was not checked. socket_create() failed: " . socket_strerror(socket_last_error())); return false; } - + $result = socket_connect($socket, $sHost, $iPort); if ($result === false) { $this->_setReturn(RESULT_ERROR, "ERROR: $sHost:$iPort failed. " . socket_strerror(socket_last_error($socket))); @@ -318,24 +330,39 @@ class appmonitorcheck { socket_close($socket); return true; } - } + } + /** - * DEPRECATED - use checkPortTcp instead - * check if system is listening to a given port + * most simple check: set values + * @return type + */ + private function checkSimple($aParams) { + $this->_checkArrayKeys($aParams, "result,value"); + return $this->_setReturn((int) $aParams["result"], $aParams["value"]); + } + + /** + * check sqlite connection * @param array $aParams * array( - * "port" + * "db" * ) * @return boolean */ - private function checkListeningIp($aParams) { - $this->_checkArrayKeys($aParams, "port"); - $sResult = exec('netstat -tulen | grep ":' . $aParams["port"] . ' "'); - if ($sResult) { - $this->_setReturn(RESULT_OK, "OK: Port " . $aParams["port"] . " was found: " . $sResult); + private function checkSqliteConnect($aParams) { + $this->_checkArrayKeys($aParams, "db"); + if (!file_exists($aParams["db"])) { + $this->_setReturn(RESULT_ERROR, "ERROR: Sqlite database file " . $aParams["db"] . " does not exist."); + return false; + } + try { + // $db = new SQLite3($sqliteDB); + // $db = new PDO("sqlite:".$sqliteDB); + $o = new PDO("sqlite:" . $aParams["db"]); + $this->_setReturn(RESULT_OK, "OK: Sqlite database " . $aParams["db"] . " was connected"); return true; - } else { - $this->_setReturn(RESULT_ERROR, "ERROR: Port " . $aParams["port"] . " is not in use."); + } catch (Exception $exc) { + $this->_setReturn(RESULT_ERROR, "ERROR: Sqlite database " . $aParams["db"] . " was not connected. " . mysqli_connect_error()); return false; } } diff --git a/public_html/appmonitor/appmonitor-client.class.php b/public_html/appmonitor/classes/appmonitor-client.class.php similarity index 55% rename from public_html/appmonitor/appmonitor-client.class.php rename to public_html/appmonitor/classes/appmonitor-client.class.php index a800565fb8e0d01fbc2954b3da9b3baefaf8d06d..235b22171b2bf2adda0a746c4577049c70d60a99 100644 --- a/public_html/appmonitor/appmonitor-client.class.php +++ b/public_html/appmonitor/classes/appmonitor-client.class.php @@ -1,5 +1,6 @@ <?php -/** + +/** * APPMONITOR CLIENT<br> * <br> * THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE <br> @@ -32,31 +33,27 @@ class appmonitor { * @var int */ private $_iDefaultTtl = 300; - + /** * internal counter: greatest return value of all checks * @var type */ private $_iMaxResult = false; - + /** * responded metadata of a website * @see _createDefaultMetadata() * @var array */ private $_aMeta = array(); - + /** * repended array of all checks * @see addCheck() * @var array */ private $_aChecks = array(); - - /** - * @var array - */ - private $_aMustKeysChecks = array("name", "description", "result", "value"); + protected $_iStart = false; /** * constructor: init data @@ -74,18 +71,19 @@ class appmonitor { * @return boolean */ private function _createDefaultMetadata() { + $this->_iStart = microtime(true); $this->_aMeta = array( "host" => false, "website" => false, "ttl" => false, - "result" => false + "result" => false, + "time" => false ); // fill with default values $this->setHost(); $this->setWebsite(); $this->setTTL(); - return true; } @@ -111,7 +109,7 @@ class appmonitor { * @return bool */ public function setWebsite($s = false) { - if (!$s) { + if (!$s && isset($_SERVER["HTTP_HOST"])) { $s = $_SERVER["HTTP_HOST"]; } return $this->_aMeta["website"] = $s; @@ -160,6 +158,107 @@ class appmonitor { return $this->_aChecks[] = $aCheck; } + /** + * add an item to notifications meta data + * + * @param string $sType type ... one of email|slack + * @param type $sValue value + * @param type $sKey optional key (for key->value instead of list of values) + * @return boolean + */ + protected function _addNotification($sType, $sValue, $sKey = false) { + $sTypeCleaned = preg_replace('/[^a-z]/', '', strtolower($sType)); + if (!isset($this->_aMeta['notifications'])) { + $this->_aMeta['notifications'] = array(); + } + if (!isset($this->_aMeta['notifications'][$sTypeCleaned])) { + $this->_aMeta['notifications'][$sTypeCleaned] = array(); + } + if ($sKey) { + $this->_aMeta['notifications'][$sTypeCleaned][$sKey] = $sValue; + } else { + $this->_aMeta['notifications'][$sTypeCleaned][] = $sValue; + } + return true; + } + + /** + * add an email to notifications list + * + * @param string $sEmailAddress email address to add + * @return boolean + */ + public function addEmail($sEmailAddress) { + return $this->_addNotification('email', $sEmailAddress); + } + + /** + * Add slack channel for notification + * @param string $sLabel + * @param string $sSlackWebhookUrl + * @return type + */ + public function addSlackWebhook($sLabel, $sSlackWebhookUrl) { + return $this->_addNotification('slack', $sSlackWebhookUrl, $sLabel); + } + /** + * add a tag for grouping in the server gui + * + * @param string $sLabel + * @param string $sSlackWebhookUrl + * @return type + */ + public function addTag($sTag) { + if(!isset($this->_aMeta['tags'])){ + $this->_aMeta['tags']=array(); + } + $this->_aMeta['tags'][]=$sTag; + return true; + } + + /** + * check referers IP address if it matches any entry in the list + * requires http request; CLI is always allowed + * On deny this method exits with 403 response + * + * @param array $aAllowedIps array of allowed ip addresses / ranges + * the ip must match from the beginning, i.e. + * "127.0." will allow requests from 127.0.X.Y + */ + public function checkIp($aAllowedIps = array()) { + if (!isset($_SERVER['REMOTE_ADDR']) || !count($aAllowedIps)) { + return true; + } + $sIP = $_SERVER['REMOTE_ADDR']; + foreach ($aAllowedIps as $sIp2Check) { + if (strpos($sIP, $sIp2Check) === 0) { + return true; + } + } + header('HTTP/1.0 403 Forbidden'); + die('ERROR: Your ip address [' . $sIP . '] has no access.'); + } + + /** + * Check a token + * requires http request; CLI is always allowed + * On deny this method exits with 403 response + * + * @param type $sVarname + * @param type $sToken + * @return boolean + */ + public function checkToken($sVarname, $sToken) { + if (!isset($_GET)) { + return true; + } + if (isset($_GET[$sVarname]) && $_GET[$sVarname] === $sToken) { + return true; + } + header('HTTP/1.0 403 Forbidden'); + die('ERROR: A token is required.'); + } + // ---------------------------------------------------------------------- // getter // ---------------------------------------------------------------------- @@ -196,8 +295,9 @@ class appmonitor { if (count($aErrors)) { + header('HTTP/1.0 503 Service Unavailable'); echo "<h1>Errors detected</h1><ol><li>" . implode("<li>", $aErrors) . "</ol><hr>"; - echo "<pre>" . print_r($this->_generateOutputArray(), true) . "</pre><hr>"; + echo "<pre>" . print_r($this->getResults(), true) . "</pre><hr>"; die("ABORT"); } } @@ -210,7 +310,7 @@ class appmonitor { * get full array for response with metadata and Checks * @return type */ - private function _generateOutputArray() { + public function getResults() { return array( "meta" => $this->_aMeta, "checks" => $this->_aChecks, @@ -224,27 +324,28 @@ class appmonitor { */ public function render($bPretty = false, $bHighlight = false) { $this->_checkData(); - - // JSON_PRETTY_PRINT reqires PHP 5.4 - if (!defined('JSON_PRETTY_PRINT')) { - $bPretty=false; - } + $this->_aMeta['time'] = number_format((microtime(true) - $this->_iStart) * 1000, 3) . 'ms'; + + // JSON_PRETTY_PRINT reqires PHP 5.4 + if (!defined('JSON_PRETTY_PRINT')) { + $bPretty = false; + } if (!$bPretty) { - $bHighlight=false; - $sOut = json_encode($this->_generateOutputArray()); + $bHighlight = false; + $sOut = json_encode($this->getResults()); } else { - $sOut = json_encode($this->_generateOutputArray(), JSON_PRETTY_PRINT); + $sOut = json_encode($this->getResults(), JSON_PRETTY_PRINT); if ($bHighlight) { - $aMsg=array( - 0=>"OK", - 1=>"UNKNOWN", - 2=>"WARNING", - 3=>"ERROR" + $aMsg = array( + 0 => "OK", + 1 => "UNKNOWN", + 2 => "WARNING", + 3 => "ERROR" ); - foreach(array_keys($aMsg) as $iCode){ - $sOut = preg_replace('/(\"result\":\ '.$iCode.')/', '$1 <span class="result'.$iCode.'"> <--- '.$aMsg[$iCode].' </span>', $sOut); + foreach (array_keys($aMsg) as $iCode) { + $sOut = preg_replace('/(\"result\":\ ' . $iCode . ')/', '$1 <span class="result' . $iCode . '"> <--- ' . $aMsg[$iCode] . ' </span>', $sOut); } - + $sOut = preg_replace('/:\ \"(.*)\"/U', ': "<span style="color:#66e;">$1</span>"', $sOut); $sOut = preg_replace('/:\ ([0-9]*)/', ': <span style="color:#3a3; font-weight: bold;">$1</span>', $sOut); $sOut = preg_replace('/\"(.*)\":/U', '"<span style="color:#840;">$1</span>":', $sOut); @@ -254,27 +355,25 @@ class appmonitor { $sOut = str_replace(' ', '', $sOut); // $sOut = preg_replace('/([{}])/', '<span style="color:#a00; ">$1</span>', $sOut); // $sOut = preg_replace('/([\[\]])/', '<span style="color:#088; ">$1</span>', $sOut); - + $sOut = '<!DOCTYPE html><html><head>' - . '<style>' - - . 'body{background:#e0e8f8; color:#235; font-family: verdana,arial;}' - . 'blockquote{background:rgba(0,0,0,0.03); border-left: 0px solid rgba(0,0,0,0.06); margin: 0 0 0 3em; padding: 0; border-radius: 1em; border-top-left-radius: 0;}' - . 'blockquote blockquote:hover{; }' - . 'blockquote blockquote blockquote:hover{border-color: #808;}' - . 'pre{background:rgba(0,0,0,0.05); padding: 1em; border-radius: 1em;}' - . '.result0{background:#aca; border-right: 0em solid #080;}' - . '.result1{background:#666; border-right: 0em solid #ccc;}' - . '.result2{background:#fc9; border-right: 0em solid #860;}' - . '.result3{background:#800; border-right: 0em solid #f00;}' - - . '</style>' - . '<title>'.__CLASS__.'</title>' - . '</head><body>' - . '<h1>'.__CLASS__.' :: debug</h1>' - . '<pre>' - . $sOut - . '</pre></body></html>'; + . '<style>' + . 'body{background:#e0e8f8; color:#235; font-family: verdana,arial;}' + . 'blockquote{background:rgba(0,0,0,0.03); border-left: 0px solid rgba(0,0,0,0.06); margin: 0 0 0 3em; padding: 0; border-radius: 1em; border-top-left-radius: 0;}' + . 'blockquote blockquote:hover{; }' + . 'blockquote blockquote blockquote:hover{border-color: #808;}' + . 'pre{background:rgba(0,0,0,0.05); padding: 1em; border-radius: 1em;}' + . '.result0{background:#aca; border-right: 0em solid #080;}' + . '.result1{background:#666; border-right: 0em solid #ccc;}' + . '.result2{background:#fc9; border-right: 0em solid #860;}' + . '.result3{background:#800; border-right: 0em solid #f00;}' + . '</style>' + . '<title>' . __CLASS__ . '</title>' + . '</head><body>' + . '<h1>' . __CLASS__ . ' :: debug</h1>' + . '<pre>' + . $sOut + . '</pre></body></html>'; } } if (!$bHighlight) { diff --git a/public_html/appmonitor/general_include.php b/public_html/appmonitor/general_include.php new file mode 100644 index 0000000000000000000000000000000000000000..0b7b2dac3d63797b955dce305250c1f2507f8e97 --- /dev/null +++ b/public_html/appmonitor/general_include.php @@ -0,0 +1,41 @@ +<?php +/* ______________________________________________________________________ + * + * A P P M O N I T O R :: CLIENT - CHECK :: GENERAL INCLUDE + * ______________________________________________________________________ + * + * @author: Axel Hahn + * ---------------------------------------------------------------------- + * 2018-06-30 v0.1 + */ + +// ---------------------------------------------------------------------- +// SECURITY STUFF ... protect access to monitoring data +// ---------------------------------------------------------------------- + +// --- restrict ip access + +// check local ips and IML networks (includes the monitor) +// appmonitor is not available on EDUROAM or VPN +$oMonitor->checkIp(array( + '127.0.0.1', + '::1', + '10.0.2.2', + '130.92.30.11', + '130.92.30.44', + '130.92.79.49', +)); + +// --- check a token +// an incoming request must have the GET param "token=123" +// $oMonitor->checkTokem('token', '123'); + + + +// ---------------------------------------------------------------------- +// NOTIFICATION +// ---------------------------------------------------------------------- + +// $oMonitor->addEmail('sysadmin@example.com'); +// $oMonitor->addSlackWebhook(array("mywebhook"=> "https://hooks.slack.com/services/(...)")); +$oMonitor->addEmail('axel.hahn@iml.unibe.ch'); \ No newline at end of file diff --git a/public_html/appmonitor/index.php b/public_html/appmonitor/index.php index 303c2c67b7dae7cd8aa51e05a7fab0394e989734..50acd21f88e8358409a1da7bb7d5b33c749ad143 100644 --- a/public_html/appmonitor/index.php +++ b/public_html/appmonitor/index.php @@ -1,7 +1,9 @@ <?php -require_once('appmonitor-client.class.php'); +require_once('classes/appmonitor-client.class.php'); $oMonitor = new appmonitor(); +@include 'general_include.php'; + $oMonitor->addCheck( array( @@ -19,6 +21,62 @@ $oMonitor->addCheck( require_once '../../config/inc_projects_config.php'; +// ---------------------------------------------------------------------- +// needed directories +// ---------------------------------------------------------------------- + + +$oMonitor->addCheck( + array( + "name" => "tmp subdir", + "description" => "Check storage for temp directories, git checkouts for logmessages exists and is writable", + "check" => array( + "function" => "File", + "params" => array( + "filename" => $aConfig['tmpDir'], + "dir" => true, + "writable" => true, + ), + ), + ) +); +$oMonitor->addCheck( + array( + "name" => "workdir", + "description" => "Check if base workdir exists and is writable", + "check" => array( + "function" => "File", + "params" => array( + "filename" => $aConfig['workDir'], + "dir" => true, + "writable" => true, + ), + ), + ) +); + + +foreach(array('dataDir', 'buildDir', 'packageDir', 'archiveDir') as $sDirKey){ + $oMonitor->addCheck( + array( + "name" => "dir [$sDirKey]", + "description" => "Check if workdir $sDirKey exists and is writable", + "check" => array( + "function" => "File", + "params" => array( + "filename" => $aConfig[$sDirKey], + "dir" => true, + "writable" => true, + ), + ), + ) + ); + +} + +// ---------------------------------------------------------------------- +// databases +// ---------------------------------------------------------------------- $sSqlitefile=$aConfig['dataDir'].'/database/logs.db'; $oMonitor->addCheck( array( @@ -32,6 +90,19 @@ $oMonitor->addCheck( ), ) ); +$sSqlitefile2=$_SERVER['DOCUMENT_ROOT'].'valuestore/data/versioncache.db'; +$oMonitor->addCheck( + array( + "name" => "Sqlite DB version cache", + "description" => "Connect sqlite db ". basename($sSqlitefile), + "check" => array( + "function" => "SqliteConnect", + "params" => array( + "db"=>$sSqlitefile2 + ), + ), + ) +); // Gesamt-Ergebnis - ohne Param=aut. max. Wert nehmen