1469 lines
43 KiB
PHP
1469 lines
43 KiB
PHP
|
<?php
|
||
|
|
||
|
//-----------------------------------------------------------------------
|
||
|
// LegacyWorlds Beta 5
|
||
|
// Engine ticks
|
||
|
//
|
||
|
// main/ticks/deathofrats/library.inc
|
||
|
//
|
||
|
// This tick is the Death of Rats.
|
||
|
//
|
||
|
// Ok, actually, it's the AMS. It checks for open proxies and other
|
||
|
// signs of suspicious behaviour.
|
||
|
//
|
||
|
// Copyright(C) 2004-2008, DeepClone Development
|
||
|
//-----------------------------------------------------------------------
|
||
|
|
||
|
|
||
|
class main_ticks_deathofrats_library {
|
||
|
|
||
|
/** How long does punishment last? */
|
||
|
const punishmentDays = 20;
|
||
|
/** How long before we reaxime an account after it's been warned? */
|
||
|
const postWarningGrace = 7;
|
||
|
/** Delay in hours between ingame checks on suspicious accounts */
|
||
|
const inGameCheckDelay = 4;
|
||
|
|
||
|
|
||
|
/** Levels for attempts to conceal pass-sharing */
|
||
|
private static $viciousLevels = array(
|
||
|
1 => array('VICIOUS-LP'),
|
||
|
2 => array('VICIOUS-MP', 'VICIOUS-LP'),
|
||
|
3 => array('VICIOUS-HP', 'VICIOUS-MP', 'VICIOUS-LP'),
|
||
|
);
|
||
|
|
||
|
/** Single player badness points, days at maximum score and days with
|
||
|
* decreasing score.
|
||
|
*/
|
||
|
private static $spBadness = array(
|
||
|
// Action => array( Points, MaxDays, DecDays )
|
||
|
"PROXY" => array( 40, 30, 30 ),
|
||
|
"CLCOOK-SIP" => array( 5, 2, 1 ),
|
||
|
"CLCOOK-DIP" => array( 2, 2, 1 ),
|
||
|
"BAT" => array( 80, 60, 60 )
|
||
|
);
|
||
|
|
||
|
/** Multiplayer badness points, days at maximum score and days with
|
||
|
* decreasing score.
|
||
|
*/
|
||
|
private static $mpBadness = array(
|
||
|
// Action => array( Points, MaxDays, DecDays )
|
||
|
"SIMPLE" => array( 15, 6, 1 ),
|
||
|
"SIMPLE-10" => array( 18, 6, 1 ),
|
||
|
"VICIOUS-LP" => array( 15, 3, 3 ),
|
||
|
"VICIOUS-MP" => array( 30, 7, 7 ),
|
||
|
"VICIOUS-HP" => array( 50, 21, 15 ),
|
||
|
"PASS" => array( 90, 90, 90 ),
|
||
|
"NOPASS" => array( -20, 90, 30 )
|
||
|
);
|
||
|
|
||
|
/** In-game badness points, days at maximum score and days with
|
||
|
* decreasing score.
|
||
|
*/
|
||
|
private static $igBadness = array(
|
||
|
// Action => array( Points, MaxDays, DecDays )
|
||
|
"LSE" => array( 5, 3, 1 ),
|
||
|
"SE" => array( 10, 4, 3 ),
|
||
|
"HSE" => array( 15, 5, 5 ),
|
||
|
"VHSE" => array( 20, 8, 7 )
|
||
|
);
|
||
|
|
||
|
private $lastExec;
|
||
|
private $now;
|
||
|
private $connections;
|
||
|
private $perAccount;
|
||
|
private $proxiedAccounts;
|
||
|
private $nEvents;
|
||
|
private $browserNames;
|
||
|
private $nChanges;
|
||
|
private $userPlays = array();
|
||
|
|
||
|
public function __construct($lib) {
|
||
|
$this->lib = $lib;
|
||
|
$this->game = $this->lib->game;
|
||
|
$this->db = $this->game->db;
|
||
|
config::$main['debug'] = 2;
|
||
|
}
|
||
|
|
||
|
public function runTick() {
|
||
|
$this->nEvents = $this->nChanges = 0;
|
||
|
$this->now = time();
|
||
|
$this->browserNames = array();
|
||
|
|
||
|
// Fetch the time at which the Death of Rats was last activated
|
||
|
sleep(1);
|
||
|
$this->lastExec = $this->db->safeTransaction(array($this, "getLastExec"));
|
||
|
if ($this->lastExec == $this->now) {
|
||
|
l::notice("Not running the Death of Rats - last execution is now");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Execute account-level checks
|
||
|
$this->accountChecks();
|
||
|
|
||
|
// Execute in-game checks
|
||
|
$ingameChecks = $this->db->safeTransaction(array($this, 'getInGameCheckList'));
|
||
|
if (count($ingameChecks)) {
|
||
|
l::debug("Executing in-game checks");
|
||
|
foreach ($ingameChecks as $pair) {
|
||
|
$this->db->safeTransaction(array($this, 'checkInGamePair'), $pair);
|
||
|
}
|
||
|
}
|
||
|
$finalPoints = $this->db->safeTransaction(array($this, 'finalizeInGameData'));
|
||
|
|
||
|
// Take action if needed
|
||
|
foreach ($finalPoints as $id => $points) {
|
||
|
if ($points < 100) {
|
||
|
continue;
|
||
|
}
|
||
|
$accounts = explode(',', $id);
|
||
|
$this->db->safeTransaction(array($this, 'takeAction'), $accounts);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** This method performs account-level checks.
|
||
|
*/
|
||
|
private function accountChecks() {
|
||
|
// If the Death of Rats had never been activated, act as if it had
|
||
|
// been one hour earlier, and check accounts with identical
|
||
|
// password.
|
||
|
if (is_null($this->lastExec)) {
|
||
|
l::notice("First execution, checking identical passwords");
|
||
|
$this->lastExec = $this->now - 3600;
|
||
|
$this->db->safeTransaction(array($this, 'checkIdenticalPassword'));
|
||
|
} else {
|
||
|
l::debug("Checking password changes");
|
||
|
$this->nChanges += $this->db->safeTransaction(array($this, 'checkPassChange'));
|
||
|
}
|
||
|
|
||
|
// For each player who logged in or created an account, check
|
||
|
// single-player suspicious behaviour.
|
||
|
$this->connections = $this->db->safeTransaction(array($this, 'getLastLogons'));
|
||
|
$this->nChanges += count($this->connections);
|
||
|
if (! $this->nChanges) {
|
||
|
l::info("No new records, skipping account-level checks.");
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// If we had connection records...
|
||
|
if (!empty($this->connections)) {
|
||
|
l::debug("Analysing " . count($this->connections) . " new record(s)");
|
||
|
$this->makePerAccountRecords();
|
||
|
|
||
|
// Start with open proxies
|
||
|
l::debug("Checking for open proxies ...");
|
||
|
$this->checkOpenProxies();
|
||
|
if (count($this->proxiedAccounts)) {
|
||
|
l::info("Logging " . count($this->proxiedAccounts) . " account(s) using open proxies");
|
||
|
$this->db->safeTransaction(array($this, 'logOpenProxies'));
|
||
|
}
|
||
|
|
||
|
// Now examine per-account entries to find different types of rats
|
||
|
l::debug("Checking single player badness");
|
||
|
foreach ($this->perAccount as $records) {
|
||
|
$this->db->safeTransaction(array($this, 'checkSinglePlayer'), array($records));
|
||
|
}
|
||
|
|
||
|
// Check for different players logging on from the same IP and on the same tracking
|
||
|
l::debug("Checking simple multiing / pass sharing");
|
||
|
$this->db->safeTransaction(array($this, 'checkSimpleMultis'));
|
||
|
|
||
|
// Check for players who clear cookies before logging on to another account
|
||
|
l::debug("Checking vicious multiing / pass sharing");
|
||
|
$this->db->safeTransaction(array($this, 'checkViciousMultis'));
|
||
|
}
|
||
|
|
||
|
// Flush the main logs
|
||
|
$this->db->safeTransaction(array($this, 'finalizeMainData'));
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method returns the last execution time of the Death of Rats.
|
||
|
*/
|
||
|
public function getLastExec() {
|
||
|
$q = $this->db->query("SELECT ts FROM dor_exec ORDER BY ts DESC LIMIT 1");
|
||
|
if (dbCount($q)) {
|
||
|
list($last) = dbFetchArray($q);
|
||
|
} else {
|
||
|
$last = null;
|
||
|
}
|
||
|
return $last;
|
||
|
}
|
||
|
|
||
|
/** This method flushes both logs and adds the execution entry
|
||
|
*/
|
||
|
public function finalizeMainData() {
|
||
|
l::debug("Updating main Death of Rats status");
|
||
|
$this->db->query(
|
||
|
"LOCK TABLE dor_exec, dor_single, dor_multi, pass_change, dor_single_points, dor_multi_points "
|
||
|
. "IN ACCESS EXCLUSIVE MODE"
|
||
|
);
|
||
|
$this->flushSinglePlayerLog();
|
||
|
$this->flushMultiPlayerLog();
|
||
|
$this->db->query(
|
||
|
"INSERT INTO dor_exec (ts, entries, events) "
|
||
|
. "VALUES ({$this->now}, {$this->nChanges}, {$this->nEvents})"
|
||
|
);
|
||
|
$this->db->query("DELETE FROM pass_change WHERE ts < {$this->now}");
|
||
|
$this->db->query("DELETE FROM dor_exec WHERE ts < {$this->now} - 7 * 24 * 3600");
|
||
|
|
||
|
$spBadness = $this->computeSinglePlayerBadness();
|
||
|
$this->computeMultiPlayerBadness($spBadness);
|
||
|
}
|
||
|
|
||
|
/** This method flushes the in-game log and computes final badness
|
||
|
* points for each player.
|
||
|
*/
|
||
|
public function finalizeInGameData() {
|
||
|
l::debug("Updating Death of Rats status for in-game checks");
|
||
|
$this->flushInGameLog();
|
||
|
return $this->computeInGameBadness();
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* ACTIONS TO PERFORM ON POSITIVE DECISIONS *
|
||
|
***********************************************************************/
|
||
|
|
||
|
/** Main action handler for a pair of accounts that have been
|
||
|
* positively identified by the Death of Rats.
|
||
|
*/
|
||
|
public function takeAction($account1, $account2) {
|
||
|
// Check and send warnings; return if no further
|
||
|
// action is required.
|
||
|
if ($this->checkWarnings($account1, $account2)) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Punish the accounts if needed
|
||
|
$this->checkPunishment($account1, $account2);
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This function checks the status of two accounts on the "warning"
|
||
|
* angle of things.
|
||
|
*/
|
||
|
private function checkWarnings($account1, $account2) {
|
||
|
// Check if both accounts were warned
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account1,MAX(ts) FROM dor_warning "
|
||
|
. "WHERE account1 IN ($account1, $account2) "
|
||
|
. "GROUP BY account1"
|
||
|
);
|
||
|
if (dbCount($q) == 0) {
|
||
|
// Both accounts need a warning
|
||
|
$this->sendWarning($account1, $account2);
|
||
|
$this->sendWarning($account2, $account1);
|
||
|
return true;
|
||
|
} elseif (dbCount($q) == 2) {
|
||
|
// Both accounts have been warned, check the grace
|
||
|
// period.
|
||
|
list($account, $mts) = dbFetchArray($q);
|
||
|
list($account, $mts2) = dbFetchArray($q);
|
||
|
$mts = ($mts > $mts2) ? $mts : $mts2;
|
||
|
return ($this->now - $mts > self::postWarningGrace * 86400);
|
||
|
}
|
||
|
|
||
|
// One of the accounts has never been warned, send him a warning
|
||
|
list($account, $mts) = dbFetchArray($q);
|
||
|
if ($account == $account1) {
|
||
|
$this->sendWarning($account2, $account1);
|
||
|
} else {
|
||
|
$this->sendWarning($account1, $account2);
|
||
|
}
|
||
|
|
||
|
return true;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method sends a warning to a player about him being flagged with
|
||
|
* another player.
|
||
|
*/
|
||
|
private function sendWarning($account1, $account2) {
|
||
|
l::trace(" Warning account #$account1");
|
||
|
$this->db->query(
|
||
|
"INSERT INTO dor_warning (account1, account2, ts) "
|
||
|
. "VALUES ($account1, $account2, {$this->now})"
|
||
|
);
|
||
|
// FIXME: should send messages in all games they play together
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method checks whether two accounts should be punished.
|
||
|
*/
|
||
|
private function checkPunishment($account1, $account2) {
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account FROM dor_punishment "
|
||
|
. "WHERE account IN ($account1, $account2) "
|
||
|
. "AND {$this->now} - ts < " . self::punishmentDays * 86400
|
||
|
);
|
||
|
|
||
|
if (dbCount($q) >= 2) {
|
||
|
// Both are already punished
|
||
|
return;
|
||
|
} elseif (dbCount($q) == 0) {
|
||
|
// Punish both accounts
|
||
|
$this->punish($account1, $account2);
|
||
|
$this->punish($account2, $account1);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Punish the account that is still unpunished
|
||
|
list($account) = dbFetchArray($q);
|
||
|
$this->punish($account == $account1 ? $account2 : $account1,
|
||
|
$account == $account1 ? $account1 : $account2);
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method punishes a player for abusive pass sharing or multiing
|
||
|
* with another player.
|
||
|
*/
|
||
|
private function punish($account1, $account2) {
|
||
|
l::trace(" PUNISHING ACCOUNT #$account1");
|
||
|
$this->db->query(
|
||
|
"INSERT INTO dor_punishment (account, other_account, ts) "
|
||
|
. "VALUES ($account1, $account2, {$this->now})"
|
||
|
);
|
||
|
// FIXME: should send messages in all games they play together
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* BADNESS POINTS COMPUTATION *
|
||
|
***********************************************************************/
|
||
|
|
||
|
private function computeSinglePlayerBadness() {
|
||
|
// Generate the array of query parts
|
||
|
$qParts = array();
|
||
|
foreach (self::$spBadness as $type => $data) {
|
||
|
array_push($qParts, "(message = '$type' AND {$this->now} - ts < "
|
||
|
. (24 * 3600 * ($data[1] + $data[2])) . ")");
|
||
|
}
|
||
|
|
||
|
// Retrieve entries
|
||
|
$qString = "SELECT * FROM dor_single WHERE " . join(" OR ", $qParts) . " ORDER BY ts";
|
||
|
$q = $this->db->query($qString);
|
||
|
|
||
|
// Compute points
|
||
|
$points = array();
|
||
|
while ($r = dbFetchHash($q)) {
|
||
|
$pPoints = $points[$r['account']];
|
||
|
|
||
|
$badness = self::$spBadness[$r['message']];
|
||
|
$bPoints = $badness[0];
|
||
|
$time = $this->now - $r['ts'];
|
||
|
|
||
|
if ($time > $badness[1] * 24 * 3600) {
|
||
|
$time -= $badness[1] * 24 * 3600;
|
||
|
$time = ($badness[2] * 24 * 3600) - $time;
|
||
|
$bPoints = round($bPoints * $time / ($badness[2] * 24 * 3600));
|
||
|
}
|
||
|
|
||
|
$points[$r['account']] = max(0, $pPoints + $bPoints);
|
||
|
}
|
||
|
|
||
|
// Insert points
|
||
|
$spData = new db_copy("dor_single_points", db_copy::copyToClean);
|
||
|
$spData->setAccessor($this->db);
|
||
|
foreach ($points as $account => $bPoints) {
|
||
|
if ($bPoints == 0) {
|
||
|
continue;
|
||
|
}
|
||
|
$spData->appendRow(array($account, $bPoints));
|
||
|
}
|
||
|
$spData->execute();
|
||
|
|
||
|
return $points;
|
||
|
}
|
||
|
|
||
|
|
||
|
private function computeMultiPlayerBadness($spBadness) {
|
||
|
// Generate the array of query parts
|
||
|
$qParts = array();
|
||
|
foreach (self::$mpBadness as $type => $data) {
|
||
|
array_push($qParts, "(message = '$type' AND {$this->now} - ts < "
|
||
|
. (24 * 3600 * ($data[1] + $data[2])) . ")");
|
||
|
}
|
||
|
|
||
|
// Retrieve entries
|
||
|
$qString = "SELECT * FROM dor_multi WHERE " . join(" OR ", $qParts) . " ORDER BY ts";
|
||
|
$q = $this->db->query($qString);
|
||
|
|
||
|
// Compute points
|
||
|
$points = array();
|
||
|
while ($r = dbFetchHash($q)) {
|
||
|
$id = $r['account1'] . ',' . $r['account2'];
|
||
|
$pPoints = $points[$id];
|
||
|
|
||
|
$badness = self::$mpBadness[$r['message']];
|
||
|
$bPoints = $badness[0];
|
||
|
$time = $this->now - $r['ts'];
|
||
|
|
||
|
if ($time > $badness[1] * 24 * 3600) {
|
||
|
$time -= $badness[1] * 24 * 3600;
|
||
|
$time = ($badness[2] * 24 * 3600) - $time;
|
||
|
$bPoints = round($bPoints * $time / ($badness[2] * 24 * 3600));
|
||
|
}
|
||
|
|
||
|
$points[$id] = max(0, $pPoints + $bPoints);
|
||
|
}
|
||
|
|
||
|
// Add single player badness to records
|
||
|
foreach ($points as $id => $bPoints) {
|
||
|
$accounts = explode(',', $id);
|
||
|
$bPoints += ceil(($spBadness[$accounts[0]] + $spBadness[$accounts[1]]) / 2);
|
||
|
$points[$id] = $bPoints;
|
||
|
}
|
||
|
|
||
|
// Insert points
|
||
|
$mpData = new db_copy("dor_multi_points", db_copy::copyToClean);
|
||
|
$mpData->setAccessor($this->db);
|
||
|
foreach ($points as $id => $bPoints) {
|
||
|
if ($bPoints == 0) {
|
||
|
continue;
|
||
|
}
|
||
|
$accounts = explode(',', $id);
|
||
|
$mpData->appendRow(array($accounts[0], $accounts[1], $bPoints));
|
||
|
}
|
||
|
$mpData->execute();
|
||
|
}
|
||
|
|
||
|
|
||
|
private function computeInGameBadness() {
|
||
|
// Generate the array of query parts
|
||
|
$qParts = array();
|
||
|
foreach (self::$igBadness as $type => $data) {
|
||
|
array_push($qParts, "(message LIKE '$type-%' AND {$this->now} - ts < "
|
||
|
. (24 * 3600 * ($data[1] + $data[2])) . ")");
|
||
|
}
|
||
|
|
||
|
// Retrieve entries
|
||
|
$qString = "SELECT * FROM dor_ingame_check WHERE " . join(" OR ", $qParts) . " ORDER BY ts";
|
||
|
$q = $this->db->query($qString);
|
||
|
|
||
|
// Compute points
|
||
|
$points = array();
|
||
|
while ($r = dbFetchHash($q)) {
|
||
|
$id = $r['account1'] . ',' . $r['account2'];
|
||
|
$pPoints = $points[$id];
|
||
|
|
||
|
list($message, $count) = explode("-", $r['message']);
|
||
|
|
||
|
$badness = self::$igBadness[$message];
|
||
|
$bPoints = $badness[0] * $count;
|
||
|
$time = $this->now - $r['ts'];
|
||
|
|
||
|
if ($time > $badness[1] * 24 * 3600) {
|
||
|
$time -= $badness[1] * 24 * 3600;
|
||
|
$time = ($badness[2] * 24 * 3600) - $time;
|
||
|
$bPoints = round($bPoints * $time / ($badness[2] * 24 * 3600));
|
||
|
}
|
||
|
|
||
|
$points[$id] = max(0, $pPoints + $bPoints);
|
||
|
}
|
||
|
|
||
|
// Insert points
|
||
|
$igData = new db_copy("dor_final_points", db_copy::copyToClean);
|
||
|
$igData->setAccessor($this->db);
|
||
|
foreach ($points as $id => $bPoints) {
|
||
|
if ($bPoints == 0) {
|
||
|
continue;
|
||
|
}
|
||
|
$accounts = explode(',', $id);
|
||
|
$igData->appendRow(array($accounts[0], $accounts[1], $bPoints));
|
||
|
}
|
||
|
$igData->execute();
|
||
|
|
||
|
return $points;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* VARIOUS HELPER METHODS *
|
||
|
***********************************************************************/
|
||
|
|
||
|
/** This method compares two IP addresses. It returns 2 if they are equal,
|
||
|
* 1 if they are on the same /24, or 0 if they don't match at all.
|
||
|
*/
|
||
|
static private function ipSimilarity($ip1, $ip2) {
|
||
|
if ($ip1 == $ip2) {
|
||
|
return 2;
|
||
|
}
|
||
|
|
||
|
$l = strrpos($ip1, ".");
|
||
|
if (is_bool($l)) {
|
||
|
l::notice("IP address without dots, that's weird ... IP was \"$ip1\"");
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
if (substr($ip1, 0, $l + 1) == substr($ip2, 0, $l + 1)) {
|
||
|
return 1;
|
||
|
}
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method returns the name of the browser associated with a
|
||
|
* tracking cookie ID.
|
||
|
*/
|
||
|
private function getBrowserName($id) {
|
||
|
if (is_null($this->browserNames[$id])) {
|
||
|
$q = $this->db->query("SELECT browser FROM web_tracking WHERE id = $id");
|
||
|
list($this->browserNames[$id]) = dbFetchArray($q);
|
||
|
}
|
||
|
return $this->browserNames[$id];
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* PASSWORDS *
|
||
|
***********************************************************************/
|
||
|
|
||
|
/** This method checks the list of accounts for identical passwords. It is
|
||
|
* only called once, the first time the Death of Rats is started.
|
||
|
*/
|
||
|
public function checkIdenticalPassword() {
|
||
|
$q = $this->db->query(
|
||
|
"SELECT password FROM account "
|
||
|
. "WHERE status IN ('STD', 'VAC', 'NEW') "
|
||
|
. "GROUP BY password "
|
||
|
. "HAVING COUNT(*) > 1"
|
||
|
);
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$qId = $this->db->query(
|
||
|
"SELECT id FROM account "
|
||
|
. "WHERE password = '" . addslashes($r[0]) . "' "
|
||
|
. "AND status IN ('STD', 'VAC', 'NEW')"
|
||
|
);
|
||
|
l::notice("Found " . dbCount($qId) . " accounts with password '{$r[0]}'");
|
||
|
|
||
|
$accounts = array();
|
||
|
while ($rID = dbFetchArray($qId)) {
|
||
|
array_push($accounts, $rID[0]);
|
||
|
}
|
||
|
|
||
|
for ($i = 0; $i < count($accounts) - 1; $i ++) {
|
||
|
for ($j = $i + 1; $j < count($accounts); $j ++) {
|
||
|
$this->multiPlayerLog("PASS", $accounts[$i], $accounts[$j]);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** This method checks for players changing their password to another
|
||
|
* player's password.
|
||
|
*/
|
||
|
public function checkPassChange() {
|
||
|
// Get all password changes
|
||
|
$q = $this->db->query("SELECT * FROM pass_change ORDER BY ts");
|
||
|
if (!dbCount($q)) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Create per-account changes list
|
||
|
$accounts = array();
|
||
|
while ($r = dbFetchHash($q)) {
|
||
|
if (!is_array($accounts[$r['account']])) {
|
||
|
$accounts[$r['account']] = array("changes" => array());
|
||
|
}
|
||
|
if (is_null($accounts[$r['account']]['initial'])) {
|
||
|
$accounts[$r['account']]['initial'] = addslashes($r['old_pass']);
|
||
|
}
|
||
|
$pass = addslashes($r['new_pass']);
|
||
|
$accounts[$r['account']]['last'] = $pass;
|
||
|
}
|
||
|
|
||
|
// For each account, verify the changes
|
||
|
foreach ($accounts as $account => $data) {
|
||
|
if ($data['initial'] == $data['last']) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Get accounts that use these passwords
|
||
|
$passwords = array();
|
||
|
if ($data['initial'] != '') {
|
||
|
$passwords[] = $data['initial'];
|
||
|
}
|
||
|
$passwords[] = $data['last'];
|
||
|
|
||
|
$q = $this->db->query(
|
||
|
"SELECT id, password FROM account "
|
||
|
. "WHERE id <> $account AND password IN ('" . join("','", $passwords) . "')"
|
||
|
);
|
||
|
if (!dbCount($q)) {
|
||
|
continue;
|
||
|
}
|
||
|
$otherList = array();
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$r[1] = addslashes($r[1]);
|
||
|
if (!is_array($otherList[$r[1]])) {
|
||
|
$otherList[$r[1]] = array($r[0]);
|
||
|
} else {
|
||
|
$otherList[$r[1]][] = $r[0];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the old password was used by another account, mark it
|
||
|
if ($data['initial'] != '' && is_array($otherList[$data['initial']])) {
|
||
|
foreach ($otherList[$data['initial']] as $account2) {
|
||
|
$this->multiPlayerLog("NOPASS", $account, $account2);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the new password is used by another account, mark it
|
||
|
if (is_array($otherList[$data['last']])) {
|
||
|
foreach ($otherList[$data['last']] as $account2) {
|
||
|
$this->multiPlayerLog("PASS", $account, $account2);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return count($accounts);
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* ACCESSING PREVIOUS RECORDS *
|
||
|
***********************************************************************/
|
||
|
|
||
|
/** This method lists the latest entries from the account_log table.
|
||
|
*/
|
||
|
public function getLastLogons() {
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account, tracking, ip_addr, action, t FROM account_log "
|
||
|
. "WHERE action IN ('IN', 'OUT', 'CREATE') "
|
||
|
. "AND t > {$this->lastExec} AND t <= {$this->now}"
|
||
|
. "ORDER BY t"
|
||
|
);
|
||
|
$records = array();
|
||
|
while ($r = dbFetchHash($q)) {
|
||
|
array_push($records, $r);
|
||
|
}
|
||
|
return $records;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method goes through all records read from the database and
|
||
|
* creates separate lists for each account.
|
||
|
*/
|
||
|
public function makePerAccountRecords() {
|
||
|
$paRecords = array();
|
||
|
|
||
|
foreach ($this->connections as $record) {
|
||
|
if (!is_array($paRecords[$record['account']])) {
|
||
|
$paRecords[$record['account']] = array();
|
||
|
}
|
||
|
array_push($paRecords[$record['account']], $record);
|
||
|
}
|
||
|
|
||
|
$this->perAccount = $paRecords;
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* OPEN PROXIES *
|
||
|
***********************************************************************/
|
||
|
|
||
|
/** This method checks for open proxies in the latest log entries.
|
||
|
*/
|
||
|
private function checkOpenProxies() {
|
||
|
$IPs = array();
|
||
|
|
||
|
// Make lists of accounts for each IP
|
||
|
foreach ($this->connections as $record) {
|
||
|
if ($record['ip_addr'] == 'AUTO' || $record['action'] == 'OUT') {
|
||
|
continue;
|
||
|
}
|
||
|
$ip = $record['ip_addr'];
|
||
|
$account = $record['account'];
|
||
|
if (!is_array($IPs[$ip])) {
|
||
|
$IPs[$ip] = array($account);
|
||
|
} elseif (!in_array($account, $IPs[$ip])) {
|
||
|
array_push($IPs[$ip], $account);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check for proxies on the IPs
|
||
|
$requests = array();
|
||
|
$proxies = array();
|
||
|
foreach (array_keys($IPs) as $ip) {
|
||
|
if (count($requests) < 20) {
|
||
|
array_push($requests, $ip);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$results = pcheck::check($requests);
|
||
|
} catch (Exception $e) {
|
||
|
l::error("Failed to check some addresses for open proxies");
|
||
|
l::info($e->getMessage());
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
foreach ($results as $host => $status) {
|
||
|
if ($status == 1) {
|
||
|
array_push($proxies, $host);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$requests = array();
|
||
|
}
|
||
|
|
||
|
// If there are some requests we didn't execute, do it
|
||
|
if (count($requests)) {
|
||
|
try {
|
||
|
$results = pcheck::check($requests);
|
||
|
} catch (Exception $e) {
|
||
|
l::error("Failed to check some addresses for open proxies");
|
||
|
l::info($e->getMessage());
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
foreach ($results as $host => $status) {
|
||
|
if ($status == 1) {
|
||
|
array_push($proxies, $host);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check for proxied accounts
|
||
|
$proxyAccounts = array();
|
||
|
foreach ($proxies as $ip) {
|
||
|
foreach ($IPs[$ip] as $account) {
|
||
|
if (in_array($account, $proxyAccounts)) {
|
||
|
continue;
|
||
|
}
|
||
|
array_push($proxyAccounts, $account);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->proxiedAccounts = $proxyAccounts;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method logs access to accounts using open proxies. A log
|
||
|
* entry is only added every 24h.
|
||
|
*/
|
||
|
public function logOpenProxies() {
|
||
|
// Get all recent open proxy logs
|
||
|
$this->db->query(
|
||
|
"SELECT account FROM dor_single "
|
||
|
. "WHERE message = 'PROXY' AND {$this->now} - ts < 86400"
|
||
|
);
|
||
|
$recent = array();
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$recent[] = $r[0];
|
||
|
}
|
||
|
|
||
|
// Insert proxy logs
|
||
|
foreach ($this->proxiedAccounts as $account) {
|
||
|
if (in_array($account, $recent)) {
|
||
|
continue;
|
||
|
}
|
||
|
$this->singlePlayerLog("PROXY", $account);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/***********************************************************************
|
||
|
* SINGLE PLAYER SUSPICIOUS BEHAVIOUR *
|
||
|
***********************************************************************/
|
||
|
|
||
|
public function checkSinglePlayer($records) {
|
||
|
// Ignore accounts that have already been kicked
|
||
|
$q = $this->db->query(
|
||
|
"SELECT status FROM account WHERE id = {$records[0]['account']}"
|
||
|
);
|
||
|
list($status) = dbFetchArray($q);
|
||
|
if ($status == 'KICKED') {
|
||
|
return;
|
||
|
}
|
||
|
l::trace("CHECKING SINGLE PLAYER {$records[0]['account']}");
|
||
|
|
||
|
// Check log-ons for people who have just tripped the "banned log-in" detector
|
||
|
$baDetected = false;
|
||
|
foreach ($records as $record) {
|
||
|
if ($record['action'] == 'OUT') {
|
||
|
continue;
|
||
|
}
|
||
|
if ($record['action'] == 'CREATE') {
|
||
|
$time = 300;
|
||
|
} else {
|
||
|
$time = 120;
|
||
|
}
|
||
|
$q = $this->db->query(
|
||
|
"SELECT * FROM banned_attempt "
|
||
|
. "WHERE ip_addr = '{$record['ip_addr']}' AND ts <= {$record['t']} "
|
||
|
. "AND {$record['t']} - ts <= $time"
|
||
|
);
|
||
|
if (dbCount($q)) {
|
||
|
$baDetected = true;
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Banned log-in attempt detected, check for a similar record in the past 2 weeks
|
||
|
if ($baDetected) {
|
||
|
$q = $this->db->query(
|
||
|
"SELECT * FROM dor_single "
|
||
|
. "WHERE account = {$records[0]['account']} AND message = 'BAT' "
|
||
|
. "AND {$this->now} - ts < 14 * 24 * 3600"
|
||
|
);
|
||
|
if (!dbCount($q)) {
|
||
|
$this->singlePlayerLog('BAT', $records[0]['account']);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// If the first record isn't the account's creation, get
|
||
|
// the previous record
|
||
|
if ($records[0]['action'] != 'CREATE') {
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account, tracking, ip_addr, action, t FROM account_log "
|
||
|
. "WHERE account = {$records[0]['account']} AND t <= {$this->lastExec} "
|
||
|
. "AND (action = 'IN' OR action = 'CREATE')"
|
||
|
. "ORDER BY t DESC LIMIT 1"
|
||
|
);
|
||
|
if (dbCount($q)) {
|
||
|
array_unshift($records, dbFetchHash($q));
|
||
|
l::trace(" Found previous action with tracking {$records[0]['tracking']}");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For each couple of records in the list, check for cookie deletion
|
||
|
$lastExam = null;
|
||
|
for ($i = 1; $i < count($records); $i ++) {
|
||
|
if (is_null($records[$i]['tracking']) || $records[$i]['ip_addr'] == 'AUTO') {
|
||
|
continue;
|
||
|
}
|
||
|
l::trace(" Checking record with tracking ID {$records[$i]['tracking']} (action '"
|
||
|
. "{$records[$i]['action']}')");
|
||
|
|
||
|
// Find previous valid record
|
||
|
for ($j = $i - 1; $j >= 0; $j --) {
|
||
|
if ($records[$j]['action'] == 'OUT' && $records[$j]['ip_addr'] == 'AUTO'
|
||
|
|| is_null($records[$j]['tracking'])
|
||
|
|| $records[$j]['tracking'] == $records[$i]['tracking']) {
|
||
|
continue;
|
||
|
}
|
||
|
break;
|
||
|
}
|
||
|
if ($j < 0 || $j === $lastExam) {
|
||
|
continue;
|
||
|
}
|
||
|
$lastExam = $j;
|
||
|
|
||
|
l::trace(" Found suspicious records: {$records[$j]['tracking']} / {$records[$i]['tracking']}");
|
||
|
|
||
|
// Check IP address
|
||
|
$ipCheck = self::ipSimilarity($records[$j]['ip_addr'], $records[$i]['ip_addr']);
|
||
|
l::trace(" IP similarity: $ipCheck");
|
||
|
if ($ipCheck == 0) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Get browser string for each tracking ID
|
||
|
$browser1 = $this->getBrowserName($records[$j]['tracking']);
|
||
|
$browser2 = $this->getBrowserName($records[$i]['tracking']);
|
||
|
$leven = levenshtein($browser1, $browser2);
|
||
|
l::trace(" Browser similarity: $leven");
|
||
|
if ($leven > 10) {
|
||
|
// *Probably* 2 different browsers, skip
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Check for earlier records that would contradict cookie deletion
|
||
|
$q = $this->db->query(
|
||
|
"SELECT COUNT(*) FROM account_log "
|
||
|
. "WHERE t < {$records[$j]['t']} AND tracking = {$records[$i]['tracking']}"
|
||
|
);
|
||
|
list($count) = dbFetchArray($q);
|
||
|
if ($count > 0) {
|
||
|
l::trace(" Found older uses of tracking ID {$records[$i]['tracking']}"
|
||
|
. ", multiple computers");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
l::trace(" ADDING RECORD");
|
||
|
$this->singlePlayerLog($ipCheck == 2 ? "CLCOOK-SIP" : "CLCOOK-DIP", $records[$i]['account']);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**********************************************
|
||
|
* MULTI PLAYERS DETECTION *
|
||
|
**********************************************/
|
||
|
|
||
|
/** This method gets all tracking cookie IDs used in the records, and
|
||
|
* generates an "extended" record including the previous X actions on
|
||
|
* each tracking record.
|
||
|
*/
|
||
|
private function extendRecord($nActions = 1) {
|
||
|
// Get the list of all tracking cookies used in the log
|
||
|
$tracking = array();
|
||
|
foreach ($this->connections as $record) {
|
||
|
if (is_null($record['tracking'])) {
|
||
|
continue;
|
||
|
}
|
||
|
if (!in_array($record['tracking'], $tracking)) {
|
||
|
$tracking[] = $record['tracking'];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (!empty($tracking)) {
|
||
|
// First we need to get the previous record for all tracking cookies
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account, tracking, ip_addr, action, t FROM account_log "
|
||
|
. "WHERE tracking IN (" . join(',', $tracking) . ") "
|
||
|
. "AND t <= {$this->lastExec} "
|
||
|
. "AND action IN ('IN', 'CREATE', 'OUT') "
|
||
|
. "ORDER BY t DESC"
|
||
|
);
|
||
|
|
||
|
// Create the list of records to merge into the log fragment
|
||
|
$extraRecords = array();
|
||
|
$okTrackings = array();
|
||
|
while (count($extraRecords) < $nActions * count($tracking) && $r = dbFetchHash($q)) {
|
||
|
if ($okTrackings[$r['tracking']] == $nActions) {
|
||
|
continue;
|
||
|
}
|
||
|
$extraRecords[] = $r;
|
||
|
$okTrackings[$r['tracking']] ++;
|
||
|
}
|
||
|
} else {
|
||
|
$extraRecords = array();
|
||
|
}
|
||
|
return array_merge(array_reverse($extraRecords), $this->connections);
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method checks for the simplest type of multi accounts,
|
||
|
* but also catches occasional pass-sharers.
|
||
|
*/
|
||
|
public function checkSimpleMultis() {
|
||
|
// Get the extra actions we need
|
||
|
$connections = $this->extendRecord();
|
||
|
|
||
|
// Extract the guys who got caught logging in on the same browser
|
||
|
// from the list of records.
|
||
|
$caught = array();
|
||
|
foreach (array_keys($this->perAccount) as $account) {
|
||
|
// Find all tracking IDs used by this account
|
||
|
l::trace("CHECKING ACCOUNT $account");
|
||
|
$aTrack = array();
|
||
|
foreach ($connections as $record) {
|
||
|
if (!is_null($record['tracking']) && $record['account'] == $account
|
||
|
&& !in_array($record['tracking'], $aTrack)) {
|
||
|
$aTrack[] = $record['tracking'];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Find all other accounts who have a record with the same tracking
|
||
|
foreach ($aTrack as $tracking) {
|
||
|
l::trace(" Checking tracking #$tracking");
|
||
|
$lastUse = null; $prevAccount = null;
|
||
|
foreach ($connections as $record) {
|
||
|
if ($tracking != $record['tracking']) {
|
||
|
continue;
|
||
|
}
|
||
|
if (is_null($lastUse)) {
|
||
|
$lastUse = $record['t'];
|
||
|
$prevAccount = $record['account'];
|
||
|
l::trace(" Last tracking use by $prevAccount");
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if ($record['account'] != $prevAccount) {
|
||
|
l::trace(" Tracking used by different account, {$record['account']}");
|
||
|
$bothAccounts = array($record['account'], $prevAccount);
|
||
|
sort($bothAccounts);
|
||
|
$bothAccounts = join(',', $bothAccounts);
|
||
|
$time = ($record['t'] - $lastUse <= 10) ? 2 : 1;
|
||
|
if ($time > $caught[$bothAccounts]) {
|
||
|
$caught[$bothAccounts] = $time;
|
||
|
}
|
||
|
$prevAccount = $record['account'];
|
||
|
}
|
||
|
$lastUse = $record['t'];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Adds the log entries
|
||
|
$nFound = 0;
|
||
|
foreach ($caught as $accounts => $type) {
|
||
|
list($a1, $a2) = explode(',', $accounts);
|
||
|
$nFound ++;
|
||
|
$this->multiPlayerLog($type == 2 ? "SIMPLE-10" : "SIMPLE", $a1, $a2);
|
||
|
}
|
||
|
if ($nFound) {
|
||
|
l::notice("$nFound simple multiing / pass sharing attempt(s) found");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method checks for vicious multi accounts: people who clear
|
||
|
* cookies before logging on to other accounts. To perform this check,
|
||
|
* the game will start by looking up 'IN' records from any account using
|
||
|
* a different tracking cookie ID but the same browser and a close IP
|
||
|
* address. For each different account found, it will check the sequence
|
||
|
* of tracking cookies used.
|
||
|
*/
|
||
|
public function checkViciousMultis() {
|
||
|
$caught = array();
|
||
|
foreach ($this->perAccount as $account => $records) {
|
||
|
l::trace(" Verifying account $account");
|
||
|
for ($i = 0; $i < count($records); $i ++) {
|
||
|
$recPair = $this->findInterestingPair($records, $i);
|
||
|
if (is_null($recPair)) {
|
||
|
continue;
|
||
|
}
|
||
|
list($R1, $R2) = $recPair;
|
||
|
l::trace(" Found pair of records: {$R1['tracking']} vs {$R2['tracking']} ("
|
||
|
. gmstrftime("%H:%M:%S %Y-%m-%d", $R1['t']) . " / "
|
||
|
. gmstrftime("%H:%M:%S %Y-%m-%d", $R2['t']) . ")");
|
||
|
|
||
|
// Get the list of logons between R1 and R2 and
|
||
|
// presenting ... troubling ... similarities
|
||
|
$R3List = $this->findIntermediaryLogons($R1, $R2);
|
||
|
if (empty($R3List)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Check the records in the list of intermediary
|
||
|
// logons.
|
||
|
foreach ($R3List as $R3) {
|
||
|
$caughtID = array($account, $R3['account']);
|
||
|
sort($caughtID);
|
||
|
$caughtID = join(',', $caughtID);
|
||
|
if ($caught[$caughtID] == 3) {
|
||
|
// We already caught this pair as
|
||
|
// VICIOUS-HP, don't recheck
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
l::trace(" Checking triplet with record from account {$R3['account']} "
|
||
|
. "(tracking {$R3['tracking']})");
|
||
|
$check = $this->checkViciousTriplet($R1, $R2, $R3);
|
||
|
if ($caught[$caughtID] < $check) {
|
||
|
$caught[$caughtID] = $check;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// For each guy we caught, add an entry to the log if one doesn't exist
|
||
|
// in the past 6 hours with a probability at least as high.
|
||
|
$nVicious = 0;
|
||
|
foreach ($caught as $ids => $level) {
|
||
|
list($id1, $id2) = explode(',', $ids);
|
||
|
$levels = self::$viciousLevels[$level];
|
||
|
|
||
|
$q = $this->db->query(
|
||
|
"SELECT COUNT(*) FROM dor_multi "
|
||
|
. "WHERE account1 = $id1 AND account2 = $id2 "
|
||
|
. "AND message IN ('" . join("','", $levels) . "') "
|
||
|
. "AND {$this->now} - ts <= 6 * 3600"
|
||
|
);
|
||
|
list($flagged) = dbFetchArray($q);
|
||
|
if ($flagged > 0) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$nVicious ++;
|
||
|
$this->multiPlayerLog($levels[0], $id1, $id2);
|
||
|
}
|
||
|
if ($nVicious) {
|
||
|
l::notice("$nVicious vicious multiing attempt(s) found");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method scans the log for two records from the same player,
|
||
|
* starting at the $i-th record in the list, where the IP is close,
|
||
|
* the browser is similar and the tracking is different.
|
||
|
*/
|
||
|
private function findInterestingPair($records, $i) {
|
||
|
$record = $records[$i];
|
||
|
if ($record['action'] != 'IN') {
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
// Try getting a previous record that matches
|
||
|
$found = null;
|
||
|
for ($j = $i - 1; $j >= 0; $j --) {
|
||
|
$oRecord = $records[$j];
|
||
|
if ($oRecord['tracking'] == $record['tracking']) {
|
||
|
// Found a previous record with the same tracking cookie
|
||
|
return null;
|
||
|
}
|
||
|
|
||
|
if ($oRecord['action'] != 'IN' || ! self::ipSimilarity($record['ip_addr'], $oRecord['ip_addr'])
|
||
|
|| $oRecord['tracking'] == $record['tracking']) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$browser1 = $this->getBrowserName($record['tracking']);
|
||
|
$browser2 = $this->getBrowserName($oRecord['tracking']);
|
||
|
if ($browser1 != $browser2 && levenshtein($brower1, $browser2) > 10) {
|
||
|
// *Probably* 2 different browsers, skip
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$found = $oRecord;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
// If we didn't find a matching record in the list, try finding
|
||
|
// one from the database
|
||
|
if (is_null($found)) {
|
||
|
$classC = substr($record['ip_addr'], 0, strrpos($record['ip_addr'], '.') - 1) . "%";
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account, tracking, ip_addr, action, t FROM account_log "
|
||
|
. "WHERE account = {$record['account']} AND action = 'IN' "
|
||
|
. "AND tracking <> {$record['tracking']} AND tracking IS NOT NULL "
|
||
|
. "AND t < {$record['t']} AND ip_addr LIKE '$classC' "
|
||
|
. "ORDER BY t DESC LIMIT 10"
|
||
|
);
|
||
|
if (!dbCount($q)) {
|
||
|
// Nothing found, return
|
||
|
}
|
||
|
while ($oRecord = dbFetchHash($q)) {
|
||
|
// Check browsers
|
||
|
$browser1 = $this->getBrowserName($record['tracking']);
|
||
|
$browser2 = $this->getBrowserName($oRecord['tracking']);
|
||
|
if ($browser1 != $browser2 && levenshtein($brower1, $browser2) > 10) {
|
||
|
// *Probably* 2 different browsers, skip
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Check if there are intermediary records
|
||
|
$q2 = $this->db->query(
|
||
|
"SELECT COUNT(*) FROM account_log "
|
||
|
. "WHERE account = {$record['account']} AND action = 'IN' "
|
||
|
. "AND tracking = {$record['tracking']} "
|
||
|
. "AND t < {$record['t']} AND t > {$oRecord['t']}"
|
||
|
);
|
||
|
if (dbCount($q2)) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
$found = $oRecord;
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
if (is_null($found)) {
|
||
|
return null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return array($record, $found);
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method checks the database for logons that present the
|
||
|
* following characteristics compared to R1 and R2:
|
||
|
* R3.account != R1.account
|
||
|
* R3.ip close to R1.ip
|
||
|
* R3.browser =~ R1.browser
|
||
|
* R3.tracking != R1.tracking
|
||
|
* R3.tracking != R2.tracking
|
||
|
* R3.timestamp BETWEEN R1.timestamp AND R2.timestamp
|
||
|
*/
|
||
|
private function findIntermediaryLogons($R1, $R2) {
|
||
|
// Get all logons matching the specifications, except
|
||
|
// for the part about the browser which must be checked
|
||
|
// manually.
|
||
|
$classC = substr($record['ip_addr'], 0, strrpos($record['ip_addr'], '.') - 1) . "%";
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account, tracking, ip_addr, action, t FROM account_log "
|
||
|
. "WHERE account <> {$R1['account']} AND tracking <> {$R1['tracking']} "
|
||
|
. "AND tracking <> {$R2['tracking']} AND ip_addr LIKE '$classC' "
|
||
|
. "AND t > {$R2['t']} AND t < {$R1['t']}"
|
||
|
);
|
||
|
if (!dbCount($q)) {
|
||
|
return array();
|
||
|
}
|
||
|
|
||
|
// Check the browser names
|
||
|
$results = array();
|
||
|
$browser1 = $this->getBrowserName($R1['tracking']);
|
||
|
while ($R3 = dbFetchHash($q)) {
|
||
|
$browser2 = $this->getBrowserName($R3['tracking']);
|
||
|
if ($browser1 == $browser2 || levenshtein($browser1, $browser2) <= 10) {
|
||
|
array_push($results, $R3);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $results;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method verifies a triplet of records and tries to determine
|
||
|
* whether they were attempts at "vicious multiing".
|
||
|
*/
|
||
|
public function checkViciousTriplet($R1, $R2, $R3) {
|
||
|
// Check if there are previous records R4 such as:
|
||
|
// R4.account = R3.account
|
||
|
// R4.tracking = R3.tracking
|
||
|
// R4.timestamp < R2.timestamp
|
||
|
$q = $this->db->query(
|
||
|
"SELECT COUNT(*) FROM account_log "
|
||
|
. "WHERE account = {$R3['account']} AND tracking = {$R3['tracking']} "
|
||
|
. "AND t < {$R3['t']}"
|
||
|
);
|
||
|
list($R4Exists) = dbFetchArray($q);
|
||
|
if ($R4Exists > 0) {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
// Get browser names
|
||
|
$browser1 = $this->getBrowserName($R1['tracking']);
|
||
|
$browser2 = $this->getBrowserName($R2['tracking']);
|
||
|
$browser3 = $this->getBrowserName($R3['tracking']);
|
||
|
|
||
|
// Compute browser similarities for (R1,R2) and (R1,R3)
|
||
|
$brSim12 = ($browser1 == $browser2) ? 2 : ((levenshtein($browser1, $browser2) <= 10) ? 1 : 0);
|
||
|
$brSim13 = ($browser1 == $browser3) ? 2 : ((levenshtein($browser1, $browser3) <= 10) ? 1 : 0);
|
||
|
$brSim23 = ($browser2 == $browser3) ? 2 : ((levenshtein($browser2, $browser3) <= 10) ? 1 : 0);
|
||
|
|
||
|
// Compute IP similarities for (R1,R2) and (R1,R3)
|
||
|
$ipSim12 = self::ipSimilarity($R1['ip_addr'], $R2['ip_addr']);
|
||
|
$ipSim13 = self::ipSimilarity($R1['ip_addr'], $R3['ip_addr']);
|
||
|
$ipSim23 = self::ipSimilarity($R2['ip_addr'], $R3['ip_addr']);
|
||
|
|
||
|
if ($ipSim12 == 2 && $ipSim13 != 2 || $brSim12 == 2 && $brSim13 != 2) {
|
||
|
return 0; // Probably not vicious multiing
|
||
|
} elseif ($ipSim12 == 2 && $ipSim13 == 2 && $brSim12 == 2 && $brSim13 == 2) {
|
||
|
return 3; // High probability
|
||
|
} elseif ($ipSim12 == 1 && ($ipSim13 == 2 || $ipSim23 == 2) && $brSim12 == 2 && $brSim13 == 2) {
|
||
|
return 3; // High probability
|
||
|
} elseif ($ipSim12 == 1 && ($ipSim13 == 2 || $ipSim23 == 2) && $brSim12 == 1
|
||
|
&& ($brSim13 == 2 || $brSim23 == 2)) {
|
||
|
return 2; // Medium probability
|
||
|
} else {
|
||
|
return 1; // Low probability
|
||
|
}
|
||
|
}
|
||
|
|
||
|
|
||
|
/**********************************************
|
||
|
* IN-GAME CHECKS *
|
||
|
**********************************************/
|
||
|
|
||
|
/** This method lists players who should be further investigated
|
||
|
* by the Death of Rats.
|
||
|
*/
|
||
|
public function getInGameCheckList() {
|
||
|
// Get all pairs of players which have a score higher than 75
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account1, account2 FROM dor_multi_points WHERE points >= 75"
|
||
|
);
|
||
|
if (!dbCount($q)) {
|
||
|
return array();
|
||
|
}
|
||
|
|
||
|
// Create a unique list
|
||
|
$potentialChecks = array();
|
||
|
$uniquePlayers = array();
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
sort($r);
|
||
|
$id = join(',', $r);
|
||
|
if (! array_key_exists($id, $potentialChecks)) {
|
||
|
$potentialChecks[$id] = $r;
|
||
|
}
|
||
|
array_push($uniquePlayers, $r[0]);
|
||
|
array_push($uniquePlayers, $r[1]);
|
||
|
}
|
||
|
$uniquePlayers = array_unique($uniquePlayers);
|
||
|
|
||
|
// Get accounts that are no longer active
|
||
|
$q = $this->db->query(
|
||
|
"SELECT id FROM account "
|
||
|
. "WHERE id IN (" . join(',', $uniquePlayers) . ") "
|
||
|
. "AND status NOT IN ('STD', 'VAC')"
|
||
|
);
|
||
|
$noCheckPlayers = array();
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$noCheckPlayers[] = $r[0];
|
||
|
}
|
||
|
|
||
|
// Check accounts that have been punished recently
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account FROM dor_punishment "
|
||
|
. "WHERE account IN (" . join(',', $uniquePlayers) . ") "
|
||
|
. "AND {$this->now} - ts <= " . (self::punishmentDays * 86400)
|
||
|
);
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$noCheckPlayers[] = $r[0];
|
||
|
}
|
||
|
|
||
|
// Check pairs that have been warned recently
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account1, account2 FROM dor_warning "
|
||
|
. "WHERE {$this->now} - ts <= " . (self::postWarningGrace * 86400)
|
||
|
);
|
||
|
$noCheckPairs = array();
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$noCheckPairs[] = join(',', $r);
|
||
|
}
|
||
|
|
||
|
// Get the list of recent checks on players
|
||
|
$q = $this->db->query(
|
||
|
"SELECT account1, account2 FROM dor_ingame_check "
|
||
|
. "WHERE {$this->now} - ts <= " . (self::inGameCheckDelay * 3600)
|
||
|
. "AND message = 'CHECK' "
|
||
|
. "GROUP BY account1, account2"
|
||
|
);
|
||
|
while ($r = dbFetchArray($q)) {
|
||
|
$noCheckPairs[] = join(',', $r);
|
||
|
}
|
||
|
|
||
|
// Create the list of pairs to check
|
||
|
$noCheckPlayers = array_unique($noCheckPlayers);
|
||
|
$noCheckPairs = array_unique($noCheckPairs);
|
||
|
$checkPairs = array();
|
||
|
foreach ($potentialChecks as $id => $accounts) {
|
||
|
if (in_array($id, $noCheckPairs) || in_array($accounts[0], $noCheckPlayers)
|
||
|
|| in_array($accounts[1], $noCheckPlayers)) {
|
||
|
continue;
|
||
|
}
|
||
|
array_push($checkPairs, $accounts);
|
||
|
}
|
||
|
|
||
|
return $checkPairs;
|
||
|
}
|
||
|
|
||
|
|
||
|
/** This method checks a pair of accounts for in-game suspicious behaviour.
|
||
|
*/
|
||
|
public function checkInGamePair($account1, $account2) {
|
||
|
foreach (config::getGames() as $game) {
|
||
|
if ($game->name == 'main' || ($game->status() != 'RUNNING' && $game->status() != 'ENDING')) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (! ($p1 = $this->doesUserPlay($game, $account1))
|
||
|
|| ! ($p2 = $this->doesUserPlay($game, $account2))) {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
// Get events and log them
|
||
|
$events = $this->runGameChecks($game, $p1, $p2);
|
||
|
if (count($events) > 1) {
|
||
|
l::trace(" Found " . (count($events) - 1) . " suspicious event(s)");
|
||
|
}
|
||
|
foreach ($events as $eType) {
|
||
|
$this->inGameLog($game->name, $eType, $account1, $account2);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/** This method checks whether an account plays a specific game.
|
||
|
*/
|
||
|
private function doesUserPlay($game, $account) {
|
||
|
if (is_null($this->userPlays[$game->name . "-" . $account])) {
|
||
|
$this->userPlays[$game->name . "-" . $account] = $game->getLib()->call(
|
||
|
'doesUserPlay', $account
|
||
|
);
|
||
|
}
|
||
|
return $this->userPlays[$game->name . "-" . $account];
|
||
|
}
|
||
|
|
||
|
/** This method runs checks on a pair of players in a specific game.
|
||
|
*/
|
||
|
private function runGameChecks($game, $p1, $p2) {
|
||
|
l::trace(" Investigating players #$p1 and #$p2 in game \"" . $game->text . "\"");
|
||
|
return $game->getLib()->call('investigate', $p1, $p2,
|
||
|
$this->now, self::inGameCheckDelay * 3600);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**********************************************
|
||
|
* LOG METHODS FOR PLAYER ENTRIES *
|
||
|
**********************************************/
|
||
|
|
||
|
private function initSinglePlayerLog() {
|
||
|
if (!is_null($this->spLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->spLog = new db_copy("dor_single", db_copy::copyTo);
|
||
|
$this->spLog->setAccessor($this->db);
|
||
|
}
|
||
|
|
||
|
private function singlePlayerLog($logType, $id) {
|
||
|
$this->initSinglePlayerLog();
|
||
|
$this->spLog->appendRow(array(
|
||
|
$logType, $id, $this->now
|
||
|
));
|
||
|
$this->nEvents ++;
|
||
|
}
|
||
|
|
||
|
private function flushSinglePlayerLog() {
|
||
|
if (is_null($this->spLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->spLog->execute();
|
||
|
$this->spLog = null;
|
||
|
}
|
||
|
|
||
|
/**********************************************
|
||
|
* LOG METHODS FOR PLAYER/PLAYER ENTRIES *
|
||
|
**********************************************/
|
||
|
|
||
|
private function initMultiPlayerLog() {
|
||
|
if (!is_null($this->mpLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->mpLog = new db_copy("dor_multi", db_copy::copyTo);
|
||
|
$this->mpLog->setAccessor($this->db);
|
||
|
}
|
||
|
|
||
|
private function multiPlayerLog($logType, $id1, $id2) {
|
||
|
$this->initMultiPlayerLog();
|
||
|
$this->mpLog->appendRow(array(
|
||
|
$logType, $id1, $id2, $this->now
|
||
|
));
|
||
|
$this->mpLog->appendRow(array(
|
||
|
$logType, $id2, $id1, $this->now
|
||
|
));
|
||
|
$this->nEvents ++;
|
||
|
}
|
||
|
|
||
|
private function flushMultiPlayerLog() {
|
||
|
if (is_null($this->mpLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->mpLog->execute();
|
||
|
$this->mpLog = null;
|
||
|
}
|
||
|
|
||
|
/**********************************************
|
||
|
* LOG METHODS FOR IN-GAME CHECKS *
|
||
|
**********************************************/
|
||
|
|
||
|
private function initInGameLog() {
|
||
|
if (!is_null($this->igLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->igLog = new db_copy("dor_ingame_check", db_copy::copyTo);
|
||
|
$this->igLog->setAccessor($this->db);
|
||
|
}
|
||
|
|
||
|
private function inGameLog($game, $logType, $id1, $id2) {
|
||
|
$this->initInGameLog();
|
||
|
$this->igLog->appendRow(array(
|
||
|
$id1, $id2, $logType, $this->now, $game
|
||
|
));
|
||
|
}
|
||
|
|
||
|
private function flushInGameLog() {
|
||
|
if (is_null($this->igLog)) {
|
||
|
return;
|
||
|
}
|
||
|
$this->igLog->execute();
|
||
|
$this->igLog = null;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
?>
|