#!/usr/bin/env php
<?php

$log = '/var/log/auth.log';

$ignore_hosts = Array(
  "yiff.myftp.org"
);
$ignore_hosts_cache = Array(); // ip => Array('ip' => <ip>, 'name' => <name>, 'expires' => <time>)
$ignore_hosts_cache_timeout = 60;

$bad_hosts = Array(); // ip => Array('expires' => <time>, 'evidence' => Array(<evidence>))
$bad_hosts_timeout = 60;
$bad_hosts_limit = 10;

openlog('StopGrind', LOG_PID|LOG_PERROR, LOG_AUTHPRIV);

function logit() {
  //printf("%s: ", strftime("%Y-%m-%d:%H:%M:%S"));
  $args = func_get_args();
  $line = call_user_func_array('sprintf', $args);
  syslog(LOG_NOTICE, $line);
  //printf("\n");
  //printf("%s: %s\n", strftime("%Y-%m-%d:%H:%M:%S"), $line);
}

function loadState() {
  logit("fixme:STUB:loadState()");
}

function saveState($state) {
  logit("fixme:STUB:saveState()");
}

function authorizedHost($ip) {
  global $ignore_hosts, $ignore_hosts_cache, $ignore_hosts_cache_timeout;
  // Expire cache entries
  $now = time();
  foreach ($ignore_hosts_cache as $key => $ignore_host)
    if ($ignore_host['expires'] < $now)
      unset($ignore_hosts_cache[$key]);
  // Now look up a cached entry
  if (isset($ignore_hosts_cache[$ip]))
    return TRUE;
  // Now look up the ignore_hosts one by one until we get a match
  foreach ($ignore_hosts as $host) {
    $record = dns_get_record($host, DNS_ANY);
    foreach($record as $entry) {
      if ($entry['ip']) {
        $ignore_hosts_cache[] = Array(
          'ip' => $entry['ip'],
          'name' => $entry['host'],
          'expires' => time() + $entry['ttl']
        );
        if ($entry['ip'] == $ip)
          return TRUE;
      }
    }
  }
  return FALSE;
}

function banBadHost($ip) {
  // Magical Gnomes
  global $bad_hosts;
  logit("bad host banned: %s", $ip);
  $cmd = sprintf("/root/siteban/autoban %s", escapeshellarg($ip));
  //exec($cmd);
  unset($bad_hosts[$ip]);
}

function badHost($ip, $line) {
  global $bad_hosts, $bad_hosts_timeout, $bad_hosts_limit;
  // Expire
  foreach($bad_hosts as $key => $value)
    if ($value['expires'] < time())
      unset($bad_hosts[$key]);

  // Check
  if (!is_array($bad_hosts[$ip]))
    $bad_hosts[$ip] = Array('evidence' => Array());
  $bad_hosts[$ip]['expires'] = time() + $bad_hosts_timeout;
  $bad_hosts[$ip]['evidence'][] = $line;
  if (count($bad_hosts[$ip]['evidence']) > $bad_hosts_limit)
    banBadHost($ip);
}

function matchLine($pattern, $key, $line) {
  global $trig_vars;
  $regs = Array();
  if ($res = preg_match($pattern, $line, $regs)) {
    $out = Array();
    foreach($trig_vars[$key] as $index => $name) {
      if ($name) $out[$name] = $regs[$index];
    }

    if (!authorizedHost($out['ip'])) {
      badHost($out['ip'], $line);
    }
  }
}

function tailLog($log) {
  global $source;
  if (is_resource($source))
    pclose($source);
  $cmd = sprintf("tail -fn 1024 %s", escapeshellarg($log));
  //logit("%s", $cmd);
  $source = popen($cmd, "r");
  // Ignore the first line, in case it's a newsyslog message.
  fgets($source, 4096);
  return $source;
}

function handle($line) {
  global $trig_pats;
  $line = trim($line);
  array_walk($trig_pats, "matchLine", $line);
}

$trig_pats = Array(
  "/Failed ([^ ]+) for( invalid user){0,1} ([^ ]+) from ([^ ]+) port ([^ ]+)/",
  "/Invalid user ([^ ]+) from ([^ ]+)/",
);
$trig_vars = Array(
  Array(
    "",
    "method",
    "",
    "username",
    "ip",
    "port"
  ),
  Array(
    "",
    "username",
    "ip"
  )
);

logit("Starting scanner");

$state = loadState();

$lines = 0;
$running = TRUE;
$buf = "";
while ($running) {
  $read = Array(STDIN);
  $write = NULL;
  $except = NULL;
  $timeout = 60;
  if ($res = stream_select($read, $write, $except, $timeout)) {
    // Handle incoming data
    if (0 == strlen($tmp = fread(STDIN, 4096))) {
      // EOF
      logit("No more input. Night night.");
      $running = FALSE;
    } else {
      $buf.= $tmp;
      while (FALSE !== ($pt = strpos($buf, "\n"))) {
        $lines++;
        $line = substr($buf, 0, $pt);
        $buf = substr($buf, $pt + 1, strlen($buf));
        handle($line);
      }
    }
  } else {
    // Timeout or interrupt. Die.
    logit("Timeout. Night night.");
    $running = FALSE;
  }
}

logit("Processed %u lines", $lines);
saveState($state);

return 0;

tailLog($log);
while ($line = fgets($source, 16384)) {
  $line = trim($line);
  // Check for newsyslog message
  if (preg_match('/newsyslog[^:]+: logfile turned over/', $line)) {
    //logit($line);
    logit("Reloading logfile\n");
    pclose($source);
    die();
    tailLog($log);
  }
  array_walk($trig_pats, "matchLine", $line);

}
pclose($source);

?>