├── README.md └── netstat.php /README.md: -------------------------------------------------------------------------------- 1 | netstat.php - simple network status 2 | =================================== 3 | 4 | netstat.php is a PHP script intended to provide a simplified, easily comprehensible and aesthetically pleasing overview of the online status of hosts and services. 5 | Background story 6 | 7 | We use the usual suspects when it comes to network monitoring. There is lots of very good open source software which does all the tricks. However, we always missed a simple, intuitive interface with lots of green traffic lights which we could make available to our users (and clients). 8 | 9 | netstat.php only checks whether a specific port is open. It can be configured to check for instance whether some software is listening on port 25 (for SMTP) but it cannot tell whether this software is still working. Technically, netstat.php uses PHP's `fsockopen()` to create an Internet or *nix domain socket connection. 10 | 11 | Point is that we do not need more since we are running full-blown monitoring software anyway. In most cases, it is sufficient for my users to see a red light only when the network or the servers are down or the daemon is not running at all. And in general, an admin will (or at least *should*) know before users do. 12 | 13 | 14 | Screenshot 15 | ---------- 16 | 17 | ![netstat.php 0.x screenshot](http://www.fam.tuwien.ac.at/~schamane/sysadmin/netstat/netstat.png) 18 | 19 | This screenshot shows also a tooltip which is included as mouse-over in case of errors. The default layout of netstat.php 1.x is practically equivalent. 20 | 21 | For an HTML example see [netstat.php 0.x's main page](http://www.fam.tuwien.ac.at/~schamane/sysadmin/netstat/). 22 | 23 | 24 | Features 25 | -------- 26 | 27 | * Checks for open ports 28 | * Can do ICMP pings 29 | * Supports IPv6 30 | * Aesthetically pleasing 31 | * Can display an optional alert message 32 | * Support for including alert messages in an RSS feed 33 | * Timeout (sensitivity) is adjustable 34 | * Self-contained but supports an extra configuration file 35 | * 2 versions available: 36 | * v0.x: Simple, straight forward, procedural code 37 | * v1.x (GitHub master): Still simple, but more features & more OO 38 | * HTML + CSS compatible with text only browsers (tested with lynx and links) 39 | * Shows timing diagnostics and error messages hidden in mouse-hovers 40 | * Free, open source, and GPL licensed 41 | * Still sticks to the KISS principle 42 | 43 | 44 | Instructions for use 45 | -------------------- 46 | 47 | netstat.php comes as a single PHP file. Download it, rename appropriately, edit the source code or create a configuration file to change settings, and have fun. Out of the box, the script looks for a configuration file named `netstat.conf.php`. It is recommended to put all settings into this file. 48 | 49 | ### netstat.conf.php 50 | 51 | Here is sort of a minimal example. For all supported settings see below. 52 | 53 | Status as of 2009-10-26 11:47
79 | Mail services are currently down. On-site engineers are already 80 | investigating the cause of this unexpected interruption. Further 81 | information will be provided in about 30 minutes.

82 | 83 | In order to disable the inclusion of the file either rename it or change its permissions. 84 | 85 | All configuration variables 86 | --------------------------- 87 | 88 | Here is a full example of a configuration file (except for `$checks` and `$htmlheader`): 89 | 90 | "; // RSS header 120 | $rsslink = $_SERVER['SCRIPT_NAME'].'?noprogress'; // RSS alert link 121 | $rssdatetime = 'o-m-d H:i:s T'; // RSS date and/or time format 122 | 123 | // $htmlheader = ... see script for a full example; note that by default it 124 | // uses other variables, too: $title, $rssheader, $online, $offline 125 | 126 | $htmlfooter = "\n\n"; // well, guess what :-) 127 | 128 | // $version ... version of script, link to homepage and author 129 | 130 | 131 | Requirements 132 | ------------ 133 | 134 | For basic operation, the script mostly uses PHP's `fsockopen()`, which needs to be enabled. Unfortunately, some web hosting providers apparently disable this function. 135 | 136 | For ICMP pings also `exec()` needs to be enabled which many providers disable, and a command line version of `ping` and/or `ping6` must be available. 137 | 138 | -------------------------------------------------------------------------------- /netstat.php: -------------------------------------------------------------------------------- 1 | 22 | * @author Andreas Schamanek 23 | * @license GPL 24 | * @copyright (c) 2012 Todd E. Johnson, Andreas Schamanek 25 | * 26 | */ 27 | 28 | 29 | 30 | // Use php netstat.php genconfig to create netstat.conf.php then 31 | // edit netstat.conf.php with your configuration. 32 | 33 | 34 | // ------------------------------------------------- functions part of script 35 | /** 36 | * Catch Exceptions 37 | * @param Exception $err 38 | */ 39 | function catchExceptions($err){ 40 | global $config; 41 | echo "Error with your request! Please try again later. " . 42 | "If the problem persists contact ".$config['contact']."."; 43 | error_log($err->__toString(),0); 44 | exit(1); 45 | } 46 | 47 | // Report no PHP errors (to be safe we include this very early) 48 | error_reporting(0); 49 | set_exception_handler('catchExceptions'); 50 | 51 | /** 52 | * Defaults for generic config 53 | * @return Default configuration 54 | */ 55 | function defaultConfig(){ 56 | $config=array(); 57 | $config['version'] = '~ git master ~ '; 58 | $config['description'] = "Online status of hosts and services provided by netstat.php"; 59 | 60 | 61 | // below we set up some silly defaults; it is recommended to save your 62 | // own settings in $configfile; if readable it will override our defaults; 63 | // for a list of all configuration variables see http://wox.at/as/_/netstat_php 64 | $config['configfile'] = 'netstat.conf.php'; 65 | 66 | // my network, title and headline of the page 67 | $config['title'] = "Our Network Status"; 68 | $config['headline'] = $config['title']; 69 | 70 | // if $alertfile exists the contents will be included()/shown (use HTML!) 71 | $config['alertfile'] = 'netstat.txt'; 72 | 73 | // checks (use pipes (|) with care ;) 74 | // syntax: host or IP to check | port | description 75 | // host/IP 76 | // IPv6 addresses must be wrapped in brackets eg [2001:db8::1]. If you 77 | // need to check a ssl service like HTTPS, SMTPS, or IMAPS you can use ssl://[2001:db8::1] 78 | // port 79 | // if $port = 'ping' an ICMP ping will be executed 80 | // if $port = 'ping6' an ICMPv6 ping will be executed 81 | // if $port = 'headline' $host is printed as a headline 82 | 83 | $config['checks'] = array( 84 | 85 | 'Examples testing localhost |headline', 86 | 'localhost | ping| ICMP ping (ping)', 87 | 'localhost | 80 | WWW server (port 80)', 88 | '127.0.0.2 | 443 | WWW server (SSL, port 443)', 89 | '127.0.0.1 | 22 | SSH server (port 22)', 90 | '127.0.0.3 | 21 | FTP server (port 21)', 91 | '-------------------------------------------------', 92 | 'Some more examples with errors or not :)|headline', 93 | 'www.hostveryunknown.com| 21|www.hostveryunknown.com', 94 | 'example.com| 23|example.com:23 (telnet is dead)', 95 | 'Empty and negative ports are ignored||', 96 | 'So are lines without pipe delimiter', 97 | 'www.google.com | 80 | WWW server @ google.com', 98 | 'localhost |-ping| Disabled ping', 99 | 'www.example.com | -80 | WWW server @ www.example.com' // no colon here! 100 | 101 | ); 102 | 103 | // exec command for ping: -l3 (preload) is recommended but 104 | //defaults($ping_command, 'ping -l3 -c3 -w1 -q'); // might not work everywhere 105 | $config['ping_command'] = 'ping -c3 -w1 -q'; 106 | $config['ping6_command'] = 'ping6 -c3 -w1 -q'; 107 | 108 | // fsockopen timeout; might need adjustment depending on network 109 | $config['timeout'] = 4; 110 | 111 | // show a very simple progress indicator (requires Javascript) 112 | // may be disabled also by adding '?noprogress' to the script's URL 113 | $config['progressindicator'] = TRUE; 114 | 115 | // strings for online and offline (by default these are used for CSS, too) 116 | $config['online'] = 'Online'; 117 | $config['offline'] = 'Offline'; 118 | 119 | // print date and/or time (leave empty to show no timestamp) 120 | $config['datetime'] = 'l, F j, Y, H:i:s T'; 121 | 122 | // RSS alert feed 123 | $config['rssfeed'] = TRUE; // use to enable or disable RSS feeds 124 | // URL of RSS feed 125 | $config['rssfeedurl'] = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'].'?rss'; 126 | // RSS feed title 127 | $config['rsstitle'] = "RSS alert feed of {$config['title']}"; 128 | // RSS header e.g. to include in $htmlheader; set to '' to offer no RSS 129 | $config['rssheader'] = '"; 130 | // RSS alert link (might point e.g. to your network status homepage) 131 | $config['rsslink'] = 'http://'.$_SERVER['HTTP_HOST'].$_SERVER['SCRIPT_NAME'].'?noprogress'; 132 | // RSS date and/or time format (here we use a ISO 8601 format) 133 | $config['rssdatetime'] = 'o-m-d H:i:s T'; 134 | 135 | // HTML Header 136 | $config['htmlheader'] = << 138 | 139 | {$config['title']} 140 | 141 | 142 | {$config['rssheader']} 143 | 162 | 163 | 164 | 165 |
166 | EOH; 167 | 168 | // HTML/page footer 169 | $config['htmlfooter'] = "
\n\n"; 170 | 171 | // Amount of time to cache the script. 0 to disable. 172 | $config['cachetime'] = 5*60; 173 | // path to writable directory we can cache in. Null or false will disable caching. 174 | $config['cachepath'] = getcwd() .'/files'; 175 | 176 | // Your support/admin contact address 177 | $config['contact'] = 'N/A'; 178 | return $config; 179 | } 180 | 181 | /** 182 | * Parse user supplied config 183 | * @param string Configuration file 184 | * @param array Configuration defaults 185 | * @return array Configuration array 186 | */ 187 | function parseConfig($file, $config){ 188 | // including $configfile if available 189 | if (file_exists($file) && is_readable($file)){ 190 | @include($file); 191 | foreach($config as $key=>$var){ 192 | if(isset($$key)){ 193 | $config[$key]=$$key; 194 | } 195 | } 196 | } 197 | 198 | return $config; 199 | } 200 | 201 | /** 202 | * Generate the default config to simplify instalation. 203 | * @param string Filename for config 204 | */ 205 | function genConfig(){ 206 | global $config; 207 | if(file_exists($config['configfile'])){ 208 | die("Config file alread exists!\n"); 209 | } 210 | $fi=fopen($_SERVER['PHP_SELF'],'r'); 211 | if($fi===false) die("Error opening ".$_SERVER['PHP_SELF']); 212 | 213 | $fo=fopen($config['configfile'],'w'); 214 | if($fo===false) die("Error opening ".$config['configfile']); 215 | fwrite($fo, "Status as of $date
248 | $message 249 |

250 | EOT; 251 | 252 | if((file_exists($config['alertfile']) && is_writable($config['alertfile'])) || is_writable(dirname($config['alertfile']))){ 253 | $fp=fopen($config['alertfile'],'w'); 254 | if($fp===false) die("Error writing alert file!"); 255 | fwrite($fp, $statusstr); 256 | fclose($fp); 257 | }else{ 258 | die("File Exists/Not Writable"); 259 | } 260 | 261 | } 262 | 263 | /** 264 | * RSS code 265 | */ 266 | function rss(){ 267 | global $config; 268 | if(!$config['rssfeed']){ 269 | // RSS requested even though it was disabled 270 | exit; 271 | } 272 | header("Content-Type: application/rss+xml"); 273 | echo "\n\n"; 274 | echo "{$config['rsstitle']}\n{$config['rsslink']}\n"; 275 | echo "{$config['rsstitle']}\n"; 276 | echo "en\n"; 277 | if (file_exists($config['alertfile']) && is_readable($config['alertfile'])) 278 | { 279 | echo "\nAlert ".date($config['rssdatetime'], filemtime($config['alertfile'])) 280 | . " for {$config['rsstitle']}\n"; 281 | echo ''.date("r", filemtime($config['alertfile']))."\n"; 282 | echo "{$config['rsslink']}\n"; 283 | echo '\n\n"; 286 | } 287 | echo "\n\n"; 288 | } 289 | 290 | /** 291 | * HTML Code 292 | * @param cache Cache object 293 | */ 294 | function html($cache){ 295 | global $config; 296 | // output HTML/page header 297 | echo $config['htmlheader']; 298 | 299 | // headline, date and time and start of table 300 | echo "

{$config['headline']}

\n"; 301 | if ($config['datetime']) echo '

as of ' . date($config['datetime']) . "

\n"; 302 | 303 | // show the contents of $alertfile if it is readable and larger than 2 bytes 304 | if (file_exists($config['alertfile']) && is_readable($config['alertfile'])) 305 | { 306 | clearstatcache(); 307 | if (filesize($config['alertfile']) > 2) 308 | { 309 | echo "
\n"; 310 | @include($config['alertfile']); 311 | echo "
\n"; 312 | } 313 | } 314 | 315 | // show a simple progress indicator 316 | if (($_REQUEST['noprogress'] !== NULL) || ($argv[1] == 'noprogress')) 317 | { 318 | $config['progressindicator'] = $FALSE; 319 | } 320 | if ($config['progressindicator']) 321 | { 322 | echo '\n"; 325 | } 326 | 327 | echo "\n"; 328 | 329 | // main loop of checks 330 | foreach ($config['checks'] as $check) 331 | { 332 | $status = $config['offline']; // default state 333 | $diagnostics = ''; // mouse-over for tooltips 334 | $output = TRUE; // print a line or print no line 335 | list($host,$port,$description) = explode('|',"$check||"); // the 2 extra '|'s are to avoid notices about undefined offsets 336 | $host = trim($host); 337 | $port = trim($port); 338 | 339 | switch ($port) 340 | { 341 | case '': // ignore lines with empty port 342 | $output = FALSE; break; 343 | case (substr($port,0,1)=='-'): // negative ports, '-ping', and 344 | // any "port" starting with '-' is considered a disabled check 345 | $output = FALSE; break; 346 | case 'headline': // print a headline within the status table 347 | // we enclose it with invisible
== == for nicer text output 348 | echo '\n"; 353 | $output = FALSE; break; 354 | case 'ping': // do an ICMP ping 355 | $ping=exec("{$config['ping_command']} $host",$pingoutput,$pingreturn); 356 | // This should continue on into ping6 as they share everything but the command. 357 | case 'ping6': // do an ICMP ping 358 | if(!isset($ping)){ 359 | $ping=exec("{$config['ping6_command']} $host",$pingoutput,$pingreturn); 360 | } 361 | if(strlen($ping)>10) 362 | { 363 | // strlen($ping)>10 works around a bug in Debian ping (", pipe 3") 364 | // http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=456192 365 | $status = $config['online']; $diagnostics = "$ping :: $pingreturn"; 366 | } 367 | else $diagnostics = "$ping :: $pingreturn"; 368 | // uncomment this if you want the full output as HTML comment 369 | //echo "\n\n"; 370 | // *nix ping command's return value meanings: 371 | // 0: all OK; 1: an error occured; 2: host unknown 372 | unset($pingoutput); 373 | unset($ping); 374 | break; 375 | default: // look if a TCP connection to port can be opened 376 | $time_start = microtime(true); 377 | $fp = @fsockopen($host, $port, $errno, $errstr, $config['timeout']); 378 | $time_end = microtime(true); 379 | $time = number_format(($time_end - $time_start)*1000,1); 380 | 381 | if ($fp) 382 | { 383 | // fsockopen worked, service is online 384 | $status = $config['online']; 385 | $diagnostics = "$time ms"; 386 | fclose($fp); 387 | } 388 | else if ($errno<0) { $diagnostics = "errno=$errno; Host unknown?"; } 389 | else { $diagnostics = $errstr; } 390 | } 391 | 392 | // output results 393 | if ($output) 394 | echo "\n"; 395 | $cache->flush(); 396 | } 397 | 398 | echo "
' 349 | . '' 350 | . $host 351 | . '' 352 | . "
$description$status
\n"; 399 | 400 | // make progress indicator disappear by means of Javascript 401 | if ($config['progressindicator']) { 402 | echo << 404 | progressindicator = document.getElementById("progress"); 405 | progressindicator.innerHTML = "asdf"; 406 | progressindicator.style.visibility = 'hidden'; 407 | 408 | EOT; 409 | } 410 | 411 | // output $version and HTML/page footer 412 | if (!empty($config['version'])) echo "

{$config['version']}

\n"; 413 | echo "{$config['htmlfooter']}\n"; 414 | 415 | } 416 | 417 | /** 418 | * Class to wrap caching and simplifiy reuse 419 | */ 420 | class cache{ 421 | 422 | /** 423 | * @var string 424 | * File to cache in 425 | */ 426 | private $file; 427 | 428 | /** 429 | * @var string 430 | * Time to keep cache 431 | */ 432 | private $time; 433 | 434 | /** 435 | * @var int 436 | * Timestamp of cache file. 437 | */ 438 | private $filetime; 439 | 440 | /** 441 | * @var string 442 | * Buffer to store the output in for later caching so the page can be flushed. 443 | */ 444 | private $buffer=''; 445 | 446 | /** 447 | * @var bool 448 | * If caching is enabled 449 | */ 450 | private $enabled=false; 451 | 452 | /** 453 | * Constructor 454 | * On new cache() run this 455 | * @param string File to cache in 456 | * @param array The config array 457 | */ 458 | public function __construct($file, $config){ 459 | if($config['cachepath']!=null && $config['cachepath']!=false && $config['cachetime']>0){ 460 | // Caching isn't disabled explicitly. Continue 461 | $this->file=$config['cachepath'].'/'.$file; 462 | $this->time=$config['cachetime']; 463 | 464 | // If output_buffering isn't small the progress indicator won't function properly 465 | $output_buffering = ini_get("output_buffering"); 466 | if($config['progressindicator']==TRUE && $output_buffering > 0){ 467 | error_log("warning: progressindicator incompatible with php.ini output_buffering > 0. Caching disabled! Create a .htaccess with the contents: php_value output_buffering \"0\""); 468 | return; 469 | } 470 | if((file_exists($this->file) && is_writable($this->file)) || is_writable(dirname($this->file))){ 471 | // We can wrte to the cache dirs enable caching. 472 | // No sense caching if we can't write! 473 | $this->enabled=true; 474 | } 475 | } 476 | } 477 | 478 | /** 479 | * Check if the file is cached and still valid 480 | * @return bool 481 | */ 482 | private function isCached(){ 483 | $this->filetime=@filemtime($this->file); 484 | if(file_exists($this->file) && (time() - $this->time <$this->filetime)){ 485 | return true; 486 | } 487 | return false; 488 | } 489 | 490 | /** 491 | * Caching Start function. 492 | * If it finds the file is already cached it will return the cached file and 493 | * exit. Otherwise will ob_start() 494 | */ 495 | public function start(){ 496 | if(!$this->enabled)return; 497 | if($this->isCached()){ 498 | // Cache is good. Return the file and exit! 499 | @include $this->file; 500 | echo ""; 501 | exit; 502 | } 503 | // Need to create cache buffer output. 504 | ob_start(); 505 | } 506 | 507 | /** 508 | * Store the page to our internal buffer and send the current page contents out. 509 | */ 510 | public function flush(){ 511 | if($this->enabled){ 512 | // Store the contents 513 | $this->buffer.=ob_get_contents(); 514 | // Flush contents of the buffer 515 | ob_flush(); 516 | } 517 | // Send to browser 518 | flush(); 519 | } 520 | 521 | /** 522 | * Caching end function 523 | * Writes the output to a file so it is cached for the next requests. 524 | */ 525 | public function end(){ 526 | if(!$this->enabled)return; 527 | if((file_exists($this->file) && is_writable($this->file)) || is_writable(dirname($this->file))){ 528 | // We can write the cache lets get the buffer contents and write it. 529 | $fp=fopen($this->file,'w'); 530 | fwrite($fp, $this->buffer . ob_get_contents()); 531 | fclose($fp); 532 | // Close the buffer and send the remaining contents to the browser 533 | ob_end_flush(); 534 | } 535 | } 536 | } 537 | 538 | 539 | 540 | 541 | // ------------------------------------------------- controler part of script 542 | 543 | // Get the default config 544 | $config = defaultConfig(); 545 | 546 | // Check first for genconfig before we try to parseConfig 547 | if($argv[1] == 'genconfig'){ 548 | genConfig(); 549 | exit; 550 | } 551 | 552 | // Parse the user's config 553 | $config = parseConfig($config['configfile'], $config); 554 | 555 | 556 | // Main Controler 557 | if($_REQUEST['rss'] !== NULL or $argv[1] == 'rss'){ 558 | // Run rss() 559 | // Create instance of cache class. 560 | $cache=new cache('rss.xml',$config); 561 | // Start the caching engine 562 | $cache->start(); 563 | // Do the actuall rss function 564 | rss(); 565 | // End the rss engine 566 | $cache->end(); 567 | }else if($argv[1] == 'setstatus'){ 568 | genStatusUpdate(); 569 | }else if($argv[1] == '-h' || $argv[1] == '--help'){ 570 | echo <<start(); 582 | // Do the actuall html function 583 | html($cache); 584 | // End the rss engine 585 | $cache->end(); 586 | } 587 | 588 | 589 | /* 590 | * License 591 | * 592 | * This script is free software; you can redistribute it and/or modify it 593 | * under the terms of the GNU General Public License as published by 594 | * the Free Software Foundation; either version 2 of the License, or 595 | * (at your option) any later version. 596 | * 597 | * This script is distributed in the hope that it will be useful, 598 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 599 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 600 | * GNU General Public License for more details. 601 | * 602 | * You should have received a copy of the GNU General Public License 603 | * along with this script; if not, write to the 604 | * Free Software Foundation, Inc., 59 Temple Place, Suite 330, 605 | * Boston, MA 02111-1307 USA 606 | 607 | * History 608 | * 609 | * 0.09 * 2009-10-26 first buggy "punish me" alpha release candidate 610 | * 0.10 * 2009-11-07 added stopwatch (time diagnostics) for online services 611 | * 0.11 * 2009-11-09 added RSS feed option for alerts 612 | * 0.12 * 2009-11-12 cleaned up and simplified settings mechanism 613 | * 0.13 * 2009-11-18 $alertfile is only included if larger than 2 bytes 614 | * 0.14 * 2009-12-05 Default CSS code change to improve font size scaling 615 | * 0.15 * 2012-08-04 added ping6 (suggested by Todd Johnson; thanks!) 616 | * 1.x * 2012-..-.. New version by Todd Johnson w/ caching, setstatus, ... 617 | * 618 | */ 619 | 620 | ?> 621 | --------------------------------------------------------------------------------