--- /dev/null
+#!/usr/bin/env php
+<?php
+/*
+ * phailomatic :: Because sometimes Python isn't an option
+ *
+ * Version 1.0, February 6, 2014
+ * Copyright (c) 2014, Ron Guerin <ron@vnetworx.net>
+ *
+ * This script implements an intrusion prevention/denial of service mitigation
+ * system in PHP.
+ *
+ * PHP Extentions: Requires: PCRE. Optional: PCNTL, POSIX
+ *
+ * Do not edit this script! Edit /etc/${scriptname}.conf to change settings.
+ * where ${scriptname} is the name of this file. Changes made to the script
+ * will get overwritten on upgrades.
+ *
+ * Phailomatic is Free Software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Phailomatic is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+ * See the GNU General Public License for more details.
+ *
+ * If you are not able to view the file COPYING, please write to the
+ * Free Software Foundation, Inc.,
+ * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+ * to get a copy of the GNU General Public License or to report a
+ * possible license violation.
+ *
+ * @package Phailomatic
+ * @author Ron Guerin <ron@vnetworx.net>
+ * @license http://www.fsf.org/licenses/gpl.html GNU Public License
+ * @copyright Copyright © 2014 Ron Guerin
+ * @filesource
+ * @link http://gothamcode.com/phailomatic Phailomatic
+ * @version 1.0
+ *
+ */
+
+// Configuration variables:
+// pidpath, hostname, checkdelay, pregex, firewall, teardown, ban, unban,
+// checkdelay, debug, daemonize, silentmax, trigger[include], trigger[exec],
+// pregex, firewall[], teardown[], ban, unban, stdout, stderr, file1, file2, ...,
+//
+// To change, edit /etc/${scriptname}.conf .
+
+// Regexes must match as a named capture the IP address of the connecting client,
+// and begin following the syslog date and severity. For example in the line:
+// Feb 6 00:30:25 localhost mail.info postfix/smtpd[23527]: ...
+// regexes should start matching with "postfix" and continue to the end of the line.
+// Please note that Phailomatic *requires* you to match to the end of the line,
+// but DO NOT include the end of line marker ($) in your regex.
+//
+// Firewall command substitutions: $1 = port number, $2 = IP address
+// Each firewall/teardown command will be executed in order
+// firewall[1]=iptables -N phailomatic-$1
+// firewall[2]=iptables -A phailomatic-$1 -j RETURN
+// firewall[3]=iptables -I INPUT -p tcp --dport $1 -j phailomatic-$1
+//
+// Triggers: Phailomatic can either call a PHP function or execute a commandline or both.
+// trigger[include]=/path/to/yourcode.php
+// This file will be included into Phailomatic and must contain:
+// function phailtrigger($ip, $ports) // IP and array of port numbers will be passed to this function when something is blocked
+//
+// trigger[exec]=/path/to/trigger $2 $1
+// when we execute this commandline, $2 will be replaced by IP, $1 by string of ports separated by commas
+//
+// Each file can contain multiple regex definitions.
+//
+// file1[path]=/var/log/messages
+// file1[syslogformat]=TRUE
+// file1[regex][1][pattern]=dropbear\[[0-9]+\]: (?:bad password|login) attempt for .* from ::ffff:(?P<ip>[0-9.]+):[0-9]+
+// file1[regex][1][ports]=22
+// file1[regex][1][hitperiod]=90
+// file1[regex][1][timeout]=600
+// file1[regex][1][bantrigger]=5
+// file1[regex][2][pattern]=postfix/smtpd\[[0-9]+\]: .*\[(?P<ip>[0-9.]+)\]: SASL (?:[A-Z]+) authentication failed: authentication failure
+// file1[regex][2][ports]=25,587,2525
+
+define('VERSION', '1.0.0');
+define('LOG_PRIO', LOG_AUTH|LOG_INFO);
+define('ERROR', 1);
+define('CANT_OPEN_CONF', 2);
+define('ALREADY_RUNNING', 3);
+define('BAD_CONFIG', 100);
+define('BAD_CONFIG_DIRS', 101);
+if (! defined('SIGKILL')) {
+ define('SIGHUP', 1);
+ define('SIGALRM', 14);
+ define('SIGTERM', 15);
+ define('SIGUSR1', 16);
+ define('SIGUSR2', 17);
+ define('SIGCHLD', 17);
+ define('SIGTSTP', 20);
+ define('SIGTTIN', 21);
+ define('SIGTTOU', 22);
+ define('SIGCONT', 25);
+}
+define('MONITOR_MAX', 5); // Max number of log files to monitor, recommend not to increase this
+
+@date_default_timezone_set(@date_default_timezone_get()); // Sadly, this is necessary
+
+$myname = preg_replace(chr(7).'.php$'.chr(7), '', basename($argv[0])); // Get my name, minus any .php extension
+openlog($myname, LOG_PID|LOG_PERROR, LOG_AUTH);
+
+// Enable garbage collection (this means PHP 5.3+ now)
+if (function_exists('gc_enable')) gc_enable(); // Collect garbage every 60 cycles below
+
+// Settings
+$confvars = array('debug' => NULL, 'pidpath' => '/var/run', 'hostname' => php_uname('n'), 'myname' => $myname,
+ 'ignoresigs' => array(SIGCHLD, SIGTSTP, SIGTTOU, SIGTTIN), 'daemonize' => TRUE,
+ 'help' => NULL, 'cleanup' => NULL,
+ 'checkdelay' => NULL, 'silentmax' => NULL, 'trigger' => NULL, 'pregex' => NULL,
+ 'firewall' => NULL, 'ban' => NULL, 'unban' => NULL, 'teardown' => NULL,
+ 'mustconf' => TRUE, 'stdin' => NULL, 'stdout' => NULL, 'stderr' => NULL, 'error' => array());
+for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+ $confvars['file'.$i] = NULL;
+ ${'iptable'.$i} = array(); // $iptable1, $iptable2, ...
+}
+
+$config = init_settings($confvars, $argv); // Get settings from defaults, config file, and commandline
+if ($config === FALSE) {
+ if ($confvars['error'][0] == CANT_OPEN_CONF) {
+ $retcode = CANT_OPEN_CONF;
+ log_msg(LOG_PRIO, $confvars, 'Error: '.((array_key_exists('myconfigname', $confvars) && $confvars['myconfigname'])
+ ? $confvars['myconfigname'] : $confvars['myname']).'.conf not found. Can\'t continue. Terminating', TRUE);
+ }
+ else {
+ foreach ($confvars['error'] as $msg) log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+ $retcode = BAD_CONFIG;
+ }
+ closelog();
+ die((int) $retcode);
+}
+
+if (isset($confvars['trigger']['include']) && ($confvars['trigger']['include'] != NULL))
+ include($confvars['trigger']['include']); // include triggercode
+
+foreach ($argv as $arg) { // Check args for --help, print if requested and exit
+ $arg = strtolower($arg);
+ if (($arg == '--help') || ($arg == '-h')) {
+ echo "Phailomatic is an intrusion-prevention/denial-of-service-mitigation daemon.\n\n";
+ echo "Options: --cleanup (cleans up firewall changes)\n";
+ echo "All other settings are expected to be specified in a configuration file.\n";
+ echo "\nPlease see the man page ".$confvars['myname']."(1) for more information.\n";
+ exit;
+ }
+ elseif ($arg == '--cleanup') {
+ $msg = 'Cleaning up firewall changes.';
+ log_msg(LOG_PRIO, $confvars, $msg);
+ firewall_configure($confvars, FALSE);
+ closelog();
+ exit;
+ }
+}
+
+// Daemonize and return lock (or just lock), handle TERM and HUP signals
+// Note to self: Open any database after forking (daemonization) to avoid pain & suffering
+if ($confvars['daemonize'] && function_exists('pcntl_fork') && function_exists('posix_setsid')) {
+ $lock = proc_daemonize($confvars['myname'], $error, $confvars['pidpath'], $confvars['ignoresigs'], $confvars['stdin'], $confvars['stdout'], $confvars['stderr']);
+ if ($lock === FALSE) {
+ switch ($error) {
+ case ALREADY_RUNNING:
+ $msg = $confvars['myname'].' already running.';
+ break;
+ case CANT_SETSID:
+ $msg = $confvars['myname'].' could not setsid.';
+ break;
+ }
+ log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+ die((int) $error);
+ }
+ pcntl_signal(SIGTERM, 'sig_handler');
+ pcntl_signal(SIGHUP, 'sig_handler');
+}
+else {
+ $confvars['stdin'] = STDIN; $confvars['stdout'] = STDOUT; $confvars['stderr'] = STDERR;
+ // Attempt to get exclusive lock on pidfile
+ $lock = fopen($confvars['pidpath'].'/'.$confvars['myname'].'.pid', 'c+');
+ if (! flock($lock, LOCK_EX | LOCK_NB)) {
+ log_msg(LOG_PRIO, $confvars, $confvars['myname'].' already running.', TRUE);
+ die((int) ALREADY_RUNNING);
+ }
+ else {
+ // Write PID to pidfile
+ fseek($lock, 0);
+ ftruncate($lock, 0);
+ fwrite($lock, getmypid());
+ fflush($lock);
+ }
+}
+
+// Now get about the point of all this...
+$cycle = 0; $debugcycle = 0; $somethingtomonitor = FALSE;
+
+for ($i=1; $i<(MONITOR_MAX+1); ++$i) if ($confvars['file'.$i] != NULL) $somethingtomonitor = TRUE;
+if (! $somethingtomonitor) {
+ $msg = 'Not configured to watch any logfiles, shutting down.';
+ log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+ closelog();
+ die($msg."\n");
+}
+
+log_msg(LOG_PRIO, $confvars, 'Phailomatic version '.VERSION.' starting, watching logs.');
+firewall_configure($confvars);
+while (TRUE) {
+ for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+ if ($confvars['file'.$i] == NULL) continue;
+ if (! isset($confvars['file'.$i]['waiting'])) $confvars['file'.$i]['waiting'] = 0;
+ $data = logfile_read($confvars['file'.$i]['path'], $confvars['file'.$i]['position']);
+ if ($data === FALSE) {
+ $confvars['file'.$i]['waiting'] = $confvars['file'.$i]['waiting'] + $confvars['checkdelay'];
+ if ($confvars['file'.$i]['waiting'] > $confvars['silentmax']) {
+ $msg = 'WARNING: Operation suspended for '.$confvars['file'.$i]['path'].' which hasn\'t existed for at least '.$confvars['silentmax'].' seconds.';
+ log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+ }
+ }
+ else {
+ if ($confvars['file'.$i]['waiting'] > $confvars['silentmax']) {
+ $msg = 'NOTICE: '.$confvars['file'.$i].' finally exists. Resuming normal operation.';
+ log_msg(LOG_PRIO, $confvars, $msg, TRUE);
+ }
+ $confvars['file'.$i]['waiting'] = 0;
+ $fails = logfile_process_data($confvars, $data, $i);
+ firewall_evaluate($confvars, $fails, ${'iptable'.$i});
+ }
+ signal_dispatch();
+ }
+
+ if ($cycle & 1) { // expire every odd numbered cycle
+ for ($i=1; $i<(MONITOR_MAX+1); ++$i) if ($confvars['file'.$i] != NULL) firewall_ban_expirer($confvars, ${'iptable'.$i});
+ }
+
+ if (++$cycle == 60) {
+ $cycle = 0;
+ if (function_exists('gc_collect_cycles')) gc_collect_cycles(); // collect garbage every 60 cycles
+ }
+
+ sleep ($confvars['checkdelay']);
+}
+// We can never get here because the while loop never ends
+
+/* *********************************************************************** */
+
+/**
+ * Reads log data from file, based on position.
+ *
+ * @package phailomatic
+ * @param string $file file to read .
+ * @param int &$position starting position
+ * @return bool $forward whether to start reading the file
+ * at the beginning or the end.
+ */
+function logfile_read($file, &$position) {
+ $data = ''; clearstatcache();
+ if (! file_exists($file)) return FALSE;
+ $cursize = filesize($file);
+ if ($cursize < $position) $position = 0;
+ if ($cursize != $position) {
+ $fh = fopen($file, 'rb');
+ fseek($fh, ($position - $cursize) - 1, SEEK_END);
+ while ((! feof($fh)) && ($position > 0)) $data .= fgets($fh, 1024);
+ fclose($fh);
+ $position = $cursize;
+ }
+ return $data;
+}
+
+/**
+ * Process log data place restrictions
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param string $data data to process from logfile read.
+ * @param int $i file number
+ * @return array $block array of ip/port combinations that should be blocked
+ */
+function logfile_process_data($confvars, $data, $fileno) {
+ $block = array();
+
+ if ($confvars['file'.$fileno]['syslogformat']) $pre = $confvars['pregex'];
+ else $pre = '';
+ foreach ($confvars['file'.$fileno]['regex'] as $key => $rule) {
+ // Look for all matches in this chunk of data
+ if (preg_match_all(chr(7).'^'.$pre.$rule['pattern'].'$'.chr(7).'m', $data, $matches) != FALSE) {
+ // Loop through matches
+ foreach ($matches['ip'] as $ip) {
+ $block[$ip]['count'] = isset($block[$ip]['count']) ? ++$block[$ip]['count'] : 1;
+ $block[$ip]['hitperiod'] = $rule['hitperiod'];
+ $block[$ip]['bantrigger'] = $rule['bantrigger'];
+ $block[$ip]['ports'] = isset($block[$ip]['ports']) ? $block[$ip]['ports'] + $rule['ports'] : $rule['ports'];
+ $block[$ip]['timeout'] = $rule['timeout'];
+ }
+ }
+ }
+ return $block;
+}
+
+/**
+ * Sends configuration commands to either set up or tear down firewall changes
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param bool $state TRUE (default) to set up, or FALSE to tear down
+ */
+function firewall_configure($confvars, $state=TRUE) {
+ if ($state == TRUE) $which = 'firewall';
+ else $which = 'teardown';
+
+ $blockport = array();
+ for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+ if ($confvars['file'.$i] == NULL) continue;
+ // this shakes out duplicate ports
+ foreach ($confvars['file'.$i]['regex'] as $rule) foreach ($rule['ports'] as $port) $blockport[$port] = TRUE;
+ }
+
+ foreach ($blockport as $port => $x) {
+ foreach ($confvars[$which] as $fwrule) {
+ exec(str_replace('$1', $port, $fwrule), $out);
+ if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+ }
+ }
+}
+
+/**
+ * Restrict access by address and port via the system firewall
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param array $fails array addresses and ports that failed authentication
+ * @param array $iptable table of restricted IPs
+ */
+function firewall_evaluate($confvars, $fails, &$iptable) {
+
+ foreach ($fails as $ip => $rule) {
+ // If count not set or expired without triggering a ban, set count to 0
+ if ((! isset($iptable[$ip]['count'])) ||
+ (($iptable[$ip]['expires'] <= date('U')) && (($iptable[$ip]['count'] + $rule['count']) < $rule['bantrigger'])))
+ $iptable[$ip]['count'] = 0;
+ $iptable[$ip]['expires'] = date('U') + $rule['hitperiod'];
+ $oldcount = $iptable[$ip]['count'];
+ $iptable[$ip]['count'] = $iptable[$ip]['count'] + $rule['count'];
+ if (($oldcount < $rule['bantrigger']) && ($iptable[$ip]['count'] >= $rule['bantrigger'])) {
+ $s = (count($rule['ports']) > 1) ? 's' : '';
+ $msg = 'Banning '.$ip.' on port'.$s.': '.implode(' ',$rule['ports']);
+ log_msg(LOG_PRIO, $confvars, $msg);
+ //echo date('M j h:i:s ').'Banning '.$ip.' on port'.$s.': '.implode(' ', $rule['ports']).' '.date('r', $authtime)."\n";
+ $base = str_replace('$2', $ip, $confvars['ban']);
+ foreach ($rule['ports'] as $port) {
+ exec(str_replace('$1', $port, $base), $out);
+ if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+ }
+ $iptable[$ip]['ports'] = isset($iptable[$ip]['ports']) ? $iptable[$ip]['ports'] + $rule['ports'] : $rule['ports'];
+
+ $iptable[$ip]['expires'] = date('U') + $rule['timeout'];
+ if (function_exists('phailtrigger')) phailtrigger($ip, $rule['ports']);
+
+ if (isset($confvars['trigger']['exec']) && ($confvars['trigger']['exec'] != NULL))
+ exec(str_replace(array('$2', '$1'), array($ip, implode(',',$rule['ports'])), $confvars['trigger']['exec']), $out);
+ }
+ }
+}
+
+/**
+ * Un-restrict access by address and port via the system firewall
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param array $iptable table of restricted IPs
+ */
+function firewall_ban_expirer($confvars, &$iptable, $all=FALSE) {
+ // If something expired, undo the ban
+ if (! is_array($iptable)) return;
+ foreach ($iptable as $ip => $state) {
+ if ($all || (isset($state['expires']) && ($state['expires'] < date('U')))) {
+ if (isset($state['ports'])) {
+ $s = (count($state['ports']) > 1) ? 's' : '';
+ $msg = 'Ending ban of '.$ip.' on port'.$s.': '.implode(' ',$state['ports']);
+ log_msg(LOG_PRIO, $confvars, $msg);
+ //echo date('M j h:i:s ').'Ending ban of '.$ip.' on port'.$s.': '.implode(' ',$state['ports'])."\n";
+ $base = str_replace('$2', $ip, $confvars['unban']);
+ foreach ($state['ports'] as $port) {
+ exec(str_replace('$1', $port, $base), $out);
+ if (isset($out[0])) log_msg(LOG_PRIO, 'ERROR: Problem ('.explode($out).') running iptables', TRUE);
+ }
+ }
+ unset($iptable[$ip]);
+ }
+ }
+}
+
+/**
+ * Flush live IP data, undo bans
+ *
+ * @package phailomatic
+ * @param int $signo signal
+ */
+function flush_live_data() {
+ // This is ugly, ugly, ugly code TODO: WTF globals
+ global $confvars;
+
+ for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+ if ($confvars['file'.$i] != NULL) {
+ global ${'iptable'.$i};
+ firewall_ban_expirer($confvars, ${'iptable'.$i}, TRUE);
+ }
+ }
+}
+
+/**
+ * Dispatch signals from PCNTL or pseudo-signals from filesystem
+ *
+ * @package phailomatic
+ * @param void
+ */
+function signal_dispatch() {
+ global $myname, $confvars;
+
+ if (function_exists('pcntl_signal_dispatch')) {
+ pcntl_signal_dispatch();
+ return;
+ }
+ elseif (file_exists('/var/run/'.$myname.'-sigterm')) {
+ unlink('/var/run/'.$myname.'-sigterm');
+ sig_handler(SIGTERM);
+ }
+ elseif (file_exists('/var/run/'.$myname.'-sighup')) {
+ unlink('/var/run/'.$myname.'-sighup');
+ sig_handler(SIGHUP);
+ }
+ elseif (file_exists('/var/run/'.$myname.'-sigusr1')) {
+ unlink('/var/run/'.$myname.'-sigusr1');
+ sig_handler(SIGUSR1);
+ }
+}
+
+/**
+ * Handle signals we might receive
+ *
+ * @package phailomatic
+ * @param int $signo signal
+ */
+function sig_handler($signo){
+ global $myname, $confvars, $argv;
+
+ switch ($signo) {
+ case SIGTERM:
+ // handle shutdown tasks
+ $msg = 'Clearing IPs and shutting down.';
+ log_msg(LOG_PRIO, $confvars, $msg);
+ firewall_configure($confvars, FALSE);
+ closelog();
+ exit;
+ break;
+ case SIGHUP:
+ // handle reload tasks
+ $msg = 'Reloading configuration settings. This also flushes bans and fail counts.';
+ log_msg(LOG_PRIO, $confvars, $msg);
+ flush_live_data();
+ init_settings($confvars, $argv);
+ case SIGUSR1:
+ // flush current IP bans and fail counts
+ $msg = 'Flushing list of banned IPs and fail counts.';
+ log_msg(LOG_PRIO, $confvars, $msg);
+ flush_live_data();
+ break;
+ }
+}
+
+/**
+ * Find location of config file and return it
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @return mixed config file path or FALSE if could not locate config file
+ */
+function config_location($confvars) {
+ // Find config file, and return it. Looks first in current directory,
+ // then iterates through array of standard directories. For each
+ // standard directory, it first looks in the standard directory, then in
+ // whatever subdirectories are specified in array $confvars['mydirs'].
+ // Tries to detect Optware, searches /opt first when detected.
+
+ $myname = (isset($confvars['myconfigname'])) ? $confvars['myconfigname'] : $confvars['myname'];
+ $mydirs = (isset($confvars['mydirs'])) ? $confvars['mydirs'] : array($myname);
+ $conf = './'.$myname.'.conf';
+ // This is a crude test for Optware/Entware
+ $return['embedded'] = (file_exists('/opt/bin/ipkg-opt') || file_exists('/opt/bin/opkg')) ? TRUE : FALSE;
+ if (file_exists($conf)) {
+ $return['path'] = $conf;
+ return $return;
+ }
+ $prefixes = ($return['embedded']) ? array('/opt', '') : array('');
+ $confdirs = array_merge((array)'', $mydirs); // '' == current directory
+ $stddirs = array('/usr/local/etc', '/etc');
+
+ foreach ($prefixes as $prefix) {
+ foreach ($stddirs as $base) {
+ foreach ($confdirs as $dir) {
+ if ($dir != '') $dir = $dir.'/';
+ $conf = $prefix.$base.'/'.$dir.$myname.'.conf';
+ if (file_exists($conf)) {
+ $return['path'] = $conf;
+ return $return;
+ }
+ }
+ }
+ }
+ $return['path'] = FALSE;
+ return $return;
+}
+
+/**
+ * Initialize settings
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @param mixed $argv command-line arguments or FALSE
+ * @return bool TRUE on success or FALSE
+ */
+function init_settings(&$confvars, $argv=FALSE) {
+ # TODO: if we configured plugins, load them so they can modify $confvars
+ // This function is generic and passes data via $confvars
+ $cmdline = TRUE; // Command line is either valid or not-applicable (web use)
+ $config = FALSE; // We haven't found a config file (yet)
+ $confvars['error'] = NULL; // We turn this into an array to pass data back
+
+ if ($argv) {
+ // Parse out any command-line args first, skipping argv[0]
+ $cmdlinevars = array();
+ $cmdlineargs = array();
+ $optsdone = FALSE;
+ $skip = TRUE;
+
+ foreach ($argv as $argnum => $arg) {
+ if ($skip) $skip = FALSE;
+ else {
+ if ($arg == '--') $optsdone = TRUE;
+ if ((substr($arg, 0, 2) == '--') && (! $optsdone)) {
+ $sep = strpos($arg,'=');
+ if ($sep !== FALSE) { // This is set to a value
+ $val = trim(substr($arg, $sep + 1));
+ $var = trim(substr($arg, 2, $sep - 2));
+ if (substr($var, -1, 1) == ']') {
+ $array=array();
+ $indexes = substr($var, strpos($var, '['));
+ $var = substr($var, 0, strpos($var, '['));
+ while (strpos($indexes, '[') !== FALSE) {
+ $b = strpos($indexes, ']');
+ $array[] = substr($indexes, 1, $b -1);
+ $indexes = substr($indexes, $b + 1);
+ }
+ }
+ }
+ else { // This is an implicit setting of TRUE
+ $var = trim(substr($arg, 2));
+ $val = TRUE;
+ }
+
+ if (array_key_exists($var, $confvars)) {
+ if ($val == '') $val = NULL;
+ if (strtoupper($val) == 'NULL') $val = NULL;
+ if (strtoupper($val) == 'TRUE') $val = TRUE;
+ if (strtoupper($val) == 'FALSE') $val = FALSE;
+ if (isset($array)) {
+ switch (count($array)) {
+ case 5:
+ $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]][$array[3]][$array[4]] = $val;
+ break;
+ case 4:
+ $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]][$array[3]] = $val;
+ break;
+ case 3:
+ $cmdlinevars[${$var}][$array[0]][$array[1]][$array[2]] = $val;
+ break;
+ case 2:
+ $cmdlinevars[${$var}][$array[0]][$array[1]] = $val;
+ break;
+ case 1:
+ #${$var}[$array[0]] = $val;
+ $cmdlinevars[${$var}][$array[0]] = $val;
+ break;
+ }
+ }
+ elseif (is_array($confvars[$var])) { // old-style array settings
+ $cmdlinevars["$var"][] = $val;
+ }
+ else {
+ $cmdlinevars["$var"] = $val;
+ }
+ }
+ else {
+ $confvars['error'][] = "Unknown command-line option --$var";
+ $cmdline = FALSE;
+ }
+ }
+ elseif ((substr($arg, 0, 1) == '-') && (strlen($arg) == 2) && (! $optsdone)) {
+ // Try to parse single letter, single dash options
+ if (in_array(substr($arg, 1), $confvars)) { // Does not accept value
+ $cmdlinevars["$var"] = TRUE;
+ }
+ elseif (array_key_exists(substr($arg, 1).':', $confvars)) { // Required value
+ if (($argnum + 1 >= $argc) || (substr($argv[$argnum + 1], 0, 1) == '-')) {
+ $confvars['error'][] = "Required value for $arg not given.";
+ $cmdline = FALSE;
+ }
+ else {
+ $var = $confvars[substr($arg, 1).':'];
+ $val = trim($argv[$argnum + 1]);
+ $skip = TRUE;
+ $cmdlinevars["$var"] = $val;
+ }
+ }
+ elseif (array_key_exists(substr($arg, 1).'::', $confvars)) { // Optional value
+ if (($argnum + 1 < $argc) && (substr($argv[$argnum + 1], 0, 1) != '-')) {
+ $var = $confvars[substr($arg, 1).'::'];
+ $val = trim($argv[$argnum + 1]);
+ $skip = TRUE;
+ $cmdlinevars["$var"] = $val;
+ }
+ else {
+ $var = $confvars[substr($arg, 1).'::'];
+ $cmdlinevars["$var"] = TRUE;
+ }
+ }
+ else {
+ $confvars['error'][] = "Unknown command-line option -$var";
+ $cmdline = FALSE;
+ }
+ }
+ else { // else it's a command argument
+ $cmdlineargs[]=$arg;
+ }
+ }
+ }
+ $confvars['arguments'] = $cmdlineargs;
+ }
+
+ // Set configuration settings with settings from config file.
+ // If config not set by command-line, look for it in the file system.
+ if (isset($cmdlinevars) && array_key_exists('config', $cmdlinevars)) {
+ if (file_exists($cmdlinevars('config'))) $config = $cmdlinevars('config');
+ else $config = FALSE;
+ }
+ else $config = config_location($confvars); // Returns config file pathname or FALSE
+
+ if ($config['path']) {
+ $confvars['config'] = $config['path'];
+ $confvars['embedded'] = $config['embedded'];
+ $lines = file($config['path']);
+ foreach ($lines as $key => $line) {
+ $line=trim($line);
+ // Ignore blank lines and lines beginning with #, //, or ;
+ if (($line != '') && (substr($line, 0, 1) != '#') && (substr($line, 0, 2) != '//') && (substr($line, 0, 1) != ';')) {
+ $sep = strpos($line, '=');
+ if ($sep === FALSE) { // This is an implicit setting of TRUE
+ $var = $line;
+ $val = TRUE;
+ }
+ else { // This is set to a value
+ $val = trim(substr($line, $sep + 1));
+ $var = trim(substr($line, 0, $sep));
+ if (substr($var, -1, 1) == ']') {
+ $array=array();
+ $indexes = substr($var, strpos($var, '['));
+ $var = substr($var, 0, strpos($var, '['));
+ while (strpos($indexes, '[') !== FALSE) {
+ $b = strpos($indexes, ']');
+ $array[] = substr($indexes, 1, $b -1);
+ $indexes = substr($indexes, $b + 1);
+ }
+ }
+ }
+ if (array_key_exists($var, $confvars)) {
+ if ($val == '') $val = NULL;
+ if (strtoupper($val) == 'NULL') $val = NULL;
+ if (strtoupper($val) == 'TRUE') $val = TRUE;
+ if (strtoupper($val) == 'FALSE') $val = FALSE;
+ if (isset($array)) {
+ switch (count($array)) {
+ case 5:
+ $confvars[$var][$array[0]][$array[1]][$array[2]][$array[3]][$array[4]] = $val;
+ break;
+ case 4:
+ $confvars[$var][$array[0]][$array[1]][$array[2]][$array[3]] = $val;
+ break;
+ case 3:
+ $confvars[$var][$array[0]][$array[1]][$array[2]] = $val;
+ break;
+ case 2:
+ $confvars[$var][$array[0]][$array[1]] = $val;
+ break;
+ case 1:
+ #${$var}[$array[0]] = $val;
+ $confvars[$var][$array[0]] = $val;
+ break;
+ }
+ }
+ elseif (is_array($confvars[$var])) { // old-style array settings
+ $confvars["$var"][] = $val;
+ }
+ else {
+ $confvars["$var"] = $val;
+ }
+ }
+ elseif (! in_array($var, $confvars['unused'])) {
+ $confvars['error'][] = "Unknown configuration file setting: $var";
+ $config = FALSE;
+ }
+ }
+ }
+ }
+ elseif ($confvars['mustconf']) { // If must have config file, return FALSE now
+ $confvars['error'] = array(CANT_OPEN_CONF);
+ return FALSE;
+ }
+
+ if ($argv) {
+ // Override all previous settings via any command-line args
+ $confvars = array_merge($confvars, $cmdlinevars);
+ }
+
+ // Do stuff local to this script
+ $defaults = init_settings_local($confvars);
+
+ // Resolve variables, ie: variable=$someothervariable
+ do {
+ $unresolved = FALSE;
+ foreach ($confvars as $key => $value) {
+ if ((! is_array($value)) && (! is_resource($value))) {
+ if (substr($value, 0, 1) == '$') {
+ $var = substr($value, 1);
+ if (array_key_exists($var, $confvars)) {
+ $confvars[$key] = $confvars[$var];
+ if (substr($confvars[$var], 0, 1) == '$') $unresolved = TRUE;
+ }
+ }
+ }
+ }
+ } while ($unresolved);
+ return (($defaults && $config && $cmdline) ? TRUE : FALSE);
+}
+
+/**
+ * Localized extensions called by init_settings
+ *
+ * @package phailomatic
+ * @param array $confvars configuration variables
+ * @return bool TRUE on success or FALSE
+ */
+function init_settings_local(&$confvars) {
+ // Application-specific code called by init_settings()
+ // returns FALSE on error and message in $confvars['error']
+ $return = TRUE;
+
+ if ((($confvars['debug'] == NULL) ||
+ (trim($confvars['debug']) == '')) && ($confvars['debug'] !== FALSE)) $confvars['debug'] = FALSE; // Don't print debug messages
+ if (is_resource(STDIN)) { // STDIN will not be a resource if we've already closed it
+ // These can't be changed after daemonization. Restart to change standard file handles.
+ $confvars['stdin'] = '/dev/null'; // Don't allow stdin to be pointed away from /dev/null
+ if (($confvars['stdout'] == NULL) || (trim($confvars['stdout']) == '')) $confvars['stdout'] = '/dev/null';
+ if (($confvars['stderr'] == NULL) || (trim($confvars['stderr']) == '')) $confvars['stderr'] = '/dev/null';
+ }
+ if ($confvars['checkdelay'] == NULL) $confvars['checkdelay'] = 2;
+ if ($confvars['silentmax'] == NULL) $confvars['silentmax'] = 60;
+ if ($confvars['pregex'] == NULL) $confvars['pregex'] =
+ '([A-Z][a-z][a-z]) ([ 0-9][0-9]) ([0-9][0-9]:[0-9][0-9]:[0-9][0-9]) ([A-Za-z0-9]+) .* ';
+ if ($confvars['firewall'] == NULL) $confvars['firewall'] =
+ array('/usr/sbin/iptables -N phailomatic-$1', '/usr/sbin/iptables -A phailomatic-$1 -j RETURN',
+ '/usr/sbin/iptables -I INPUT -p tcp --dport $1 -j phailomatic-$1');
+ if ($confvars['ban'] == NULL) $confvars['ban'] = '/usr/sbin/iptables -I phailomatic-$1 -s $2 -j DROP';
+ if ($confvars['unban'] == NULL) $confvars['unban'] = '/usr/sbin/iptables -D phailomatic-$1 -s $2 -j DROP';
+ if ($confvars['teardown'] == NULL) $confvars['teardown'] =
+ array('/usr/sbin/iptables -D INPUT -p tcp --dport $1 -j phailomatic-$1',
+ '/usr/sbin/iptables -F phailomatic-$1', '/usr/sbin/iptables -X phailomatic-$1');
+ for ($i=1; $i<(MONITOR_MAX+1); ++$i) {
+ if ($confvars['file'.$i] == NULL) continue;
+ if (! isset($confvars['file'.$i]['syslogformat'])) $confvars['file'.$i]['syslogformat'] = TRUE;
+ foreach ($confvars['file'.$i]['regex'] as $key => $rule) {
+ if (! isset($rule['ports'])) {
+ $confvars['error'][] = 'Ports to firewall missing for '.$confvars['file'.$i].' regex['.$key.']';
+ $return = FALSE;
+ }
+ elseif (! is_array($rule['ports'])) {
+ $rule['ports'] = str_replace(' ', '', $rule['ports']);
+ $confvars['file'.$i]['regex'][$key]['ports'] = explode(',', $rule['ports']);
+ }
+
+ if (! isset($rule['timeout'])) $confvars['file'.$i]['regex'][$key]['timeout'] = 600;
+ if (! isset($rule['bantrigger'])) $confvars['file'.$i]['regex'][$key]['bantrigger'] = 5;
+ if (! isset($rule['hitperiod'])) $confvars['file'.$i]['regex'][$key]['hitperiod'] = 90;
+ }
+ }
+ return $return;
+}
+
+/**
+ * Daemonize
+ *
+ * @package phailomatic
+ * @param string $myname name of the running script
+ * @param array $error error messages passed back
+ * @param string $pidpath path to PIDfile
+ * @param array $ignoresigs array of signals to ignore
+ * @param int $stdin STDIN
+ * @param int $stdout STDOUT
+ * @param int $stderr STDERR
+ * @return bool TRUE on success or FALSE
+ */
+function proc_daemonize($myname, &$error, $pidpath, $ignoresigs, &$stdin, &$stdout, &$stderr) {
+ // Returns file descriptors in place of file paths for $stdin, $stdout, $stderr
+
+ // Fork, become session leader, fork again, and close open handles so there's no zombie.
+ // Open lock file
+ $lock = fopen($pidpath.'/'.$myname.'.pid', 'c+');
+ if (! flock($lock, LOCK_EX | LOCK_NB)) {
+ $error = ALREADY_RUNNING;
+ return FALSE;
+ }
+
+ // Fork. If we get a PID, exit. If we get 0, we're the child, continue
+ if (pcntl_fork()) exit();
+
+ // Dissociate from controlling terminal, become session leader
+ if (posix_setsid() === -1) {
+ $error = CANT_SETSID;
+ return FALSE;
+ }
+ usleep(100000); // sleep 1/10th of a second
+
+ // Fork again as session leader to be free of other processes. If pcntl_fork
+ // returns 0 we're the child, else we're the parent getting the PID of the child
+ $childpid = pcntl_fork();
+
+ if ($childpid) {
+ // If we get here, we are the parent, write PID to pidfile
+ fseek($lock, 0);
+ ftruncate($lock, 0);
+ fwrite($lock, $childpid);
+ fflush($lock);
+ closelog();
+ exit();
+ }
+ // If we get here we're the child finally independent, Grab lockfile.
+ else flock($lock, LOCK_EX|LOCK_NB); // Grab lockfile.
+
+ // As we are a daemon, close and re-open standard file descriptors.
+ proc_init_std_file_descriptors($stdin, $stdout, $stderr);
+
+ // Ignore some signals we don't care about
+ foreach ($ignoresigs as $ignoresig) pcntl_signal($ignoresig, SIG_IGN);
+ return $lock;
+}
+
+/**
+ * Initialize standard file descriptors
+ *
+ * @package phailomatic
+ * @param int $stdin STDIN
+ * @param int $stdout STDOUT
+ * @param int $stderr STDERR
+ */
+function proc_init_std_file_descriptors(&$stdin, &$stdout, &$stderr) {
+
+ if (is_resource(STDIN)) fclose(STDIN);
+ if (is_resource(STDOUT)) fclose(STDOUT);
+ if (is_resource(STDERR)) fclose(STDERR);
+ if (is_resource($stdin)) fclose($stdin);
+ if (is_resource($stdout)) fclose($stdout);
+ if (is_resource($stderr)) fclose($stderr);
+
+ // http://andytson.com/blog/2010/05/daemonising-a-php-cli-script-on-a-posix-system/
+ // When a standard file descriptor is closed, it can be replaced.
+ // Create new standard file descriptors in case anything tries to use them.
+ // Variable names are not important, but do not re-order the fopens
+ if ($stdout == $stderr) $stderr = 'php://stdout'; // hack to duplicate fd1 to fd2
+ $stdin = fopen('/dev/null', 'r'); // set fd0
+ $stdout = fopen($stdout, 'w'); // set fd1
+ $stderr = fopen($stderr, 'w'); // set fd2 or set fd2 to fd1
+}
+
+/**
+ * Log message to syslog
+ *
+ * @package phailomatic
+ * @param int $prio syslog message priority
+ * @param array $vars configuration variables
+ * @param array $msgs messages to send to syslog
+ * @param int $stderr STDERR
+ */
+function log_msgs($prio, $vars, $msgs, $stderr=FALSE) {
+ foreach ($msgs as $msg) log_msg($prio, $vars, $msg, $stderr);
+}
+
+/**
+ * Log message to syslog
+ *
+ * @package phailomatic
+ * @param int $prio syslog message priority
+ * @param array $vars configuration variables
+ * @param string $msg message to send to syslog
+ * @param int $stderr STDERR
+ */
+function log_msg($prio, $vars, $msg, $stderr=FALSE) {
+ if ($vars['debug']) $msg .= ' mem: '.memory_get_usage().' max: '.memory_get_peak_usage();
+ syslog($prio, $msg);
+ if ($stderr === TRUE) @fwrite($vars['stderr'], date('M j H:i:s ').$msg."\n");
+}
+?>