#!/usr/bin/php 
<?php
/* Prevent ban evasion and proxy usage */

/***** How it works ******/
/* This script reads a file named blacklist.txt and prevents names and ips in there from entering the server*/
/* It will adapt to new IP's and aliases; if a new IP is used by a blacklisted alias or visa-versa, it will add to the blacklist*/
/* For example, you can blacklist a clan tag and all members who join will be auto-banned no matter which IP */
/* And if they change their clan tag or name/alias, the bot will remember every IP assosiated with any name they ever used */
/* If ban evaders try and change both alias and IP, the bot will then check for proxies and ban them */
/* Sometimes there are some false positives that can't be avoided, add good ips to the blacklist: "WHITELIST 123.123.123.123"*/
/* You cannot whitelist aliases yet because then ban evaders can just use that alias. */

//Made by dukevin (dukevinjduke@gmail.com) Questions/Comments welcome. PUBLIC DOMAIN 2014


$directory = "df"; //YOUR DIRECTORY NAME
$path = "/home/duke/aa/servers/$directory/var/ladderlog.txt"; //path to ladderlog (no need to touch if using Rx Hosting)
//Put IPs or aliases you want to blacklist in this file. Partial aliases and ips will work:
$file = "/home/duke/aa/servers/$directory/var/customize/blacklist.txt"; //Location of the blacklist file
//If the blacklist file is empty, only proxy checking will be avalable
$ban_msg = "You were auto-banned for ban evasion"; //message displayed to the person we caught ban evading
$proxy_msg = "You were auto-banned for using a proxy"; //message displayed to person using a proxy
$ban_time = 99999; //time each ban is worth in minutes

$check_proxies = true; //this enables a proxy checker; there can be some false positives so some may want it off

/*********** End editable settings *************/
while(1)
{
	$line = rtrim(fgets(STDIN, 1024));
	$split = explode(" ", $line);
	$banned = file($file);
	if($split[0] == "PLAYER_ENTERED") //PLAYER_ENTERED rebirth 199.255.211.12 Rebirth
	{
		if(in_banned_list($split[1], $banned) ||is_numeric($split[1])) //if name matches
		{
			p("Name match: {$split[1]}");
			if(in_banned_list($split[2], $banned) == false) //check if IP is banned
			{ //looks like they have a new proxy, let's add it
				file_put_contents($file, $split[2]."\n", FILE_APPEND);
				p("New IP found from name {$split[2]}. Logged IP");
			}
			cout("BOT: {$split[1]} has triggered ban filter #A.");
			$banned[] = $split[1]; //add this name to current banlist
			ban($split[1], $split[2]);
		}
		else if(in_banned_list($split[2], $banned)) //if ip matches
		{
			p("New alias found from IP {$split[2]}...");
			if(strlen($split[1]) >= 3) //don't log names 2 or fewer chars
			{
					//call checkuser to see if this name is an impersonation
					check_alias($split[1]);
			}
			//else the ip matches and the name matches, so no need to do anything with ip
			cout("BOT: {$split[1]} has triggered ban filter #P.");
			$banned[] = $split[1]; //add this name to current banlist
			ban($split[1], $split[2]);
		}
		else if(is_proxy($split[2]) && $check_proxies)
		{
			check_alias($split[1]); //add the new alias if it is valid
			p("PROXY ".$split[1]." entered the game.");
			ban($split[1], $split[2], true);
			$banned[] = $split[1]; //add this name to current banlist
		}
	}
	if($split[0] == "PLAYER_RENAMED") //PLAYER_RENAMED pike pike@ct 85.202.52.21 1 pike
	{
		if(in_banned_list($split[2], $banned) || in_banned_list($split[3], $banned) ) //if rename matches name in blacklist or IP still matches
		{
			p("Alias/IP match from rename {$split[2]}. Checking prev name {$split[1]}...");
			if(in_banned_list($split[1], $banned) == false) //log old alias if it's not already logged
			{//switched to a banned name
				check_alias($split[1]);
			}
			if(in_banned_list($split[3], $banned) == false) //log ip if it's not already logged
			{
				p("Logging new IP using evidence from the rename {$split[1]} -> {$split[2]}");
				file_put_contents($file, $split[3]."\n", FILE_APPEND);	
			}
			cout("BOT: After review, {$split[1]} has been determined to trigger ban filter #R");
			$banned[] = $split[2]; //add this new name to current banlist
			ban($split[2], $split[3]);
		}
		else if(in_banned_list($split[1], $banned)) //a banned name switches to unbanned name (this case shouldn't really 
		{																							//happen but we'll log the new names if we can)
			$banned[] = $split[2]; //add this new name to current banlist
			file_put_contents($file, $split[2]."\n", FILE_APPEND);
			p("A banned name switched to an unbanned name: {$split[1]} -> {$split[2]} Haven't checked for alias usage though, just banning it");		
			ban($split[2], $split[3]);
		}
	}
	/*if($split[0] == "CYCLE_CREATED") //commented out because it interferes with whitelist but it also checks for banned players during cycle spawning
	{ //not really necessary but will help if there is no whitelist and you just loaded scripts. Otherwise not useful.
		if(in_banned_list($split[1], $banned))
		{
			p("BLACKLISTED ".$split[1]." cycle_created event. Banned");
			echo "ban {$split[1]} 99999 You were auto-banned for being either swag or zulu.\n";
			cout("0xff8080{$split[1]} has been auto-banned for being either swag or zulu.");
		}
	}*/
	if($split[0] == "ROUND_COMMENCING")
	{
		if(!file_exists($file)) {
			cout("BOT: The blacklist file cannot be found at $file");
			cout("Only proxy checking is enabled, but alias/ip saving is disabled.");
		}
		unset($banned);
		$banned = file($file);
	}
}
//checks if a string is in the banned list. Will match partial strings
function in_banned_list($str, $list)
{
	$str = strtolower(trim($str));
	if(empty($list))
	{
		cout("BOT: Cannot read file or empty ban list");
		return false;
	}
	foreach($list as $i)
	{
		$i = strtolower(trim($i));
		if(stripos($str, $i) !== false)
		{
			p("Match: $str -> $i");
			return true;
		}
	}
	return false;
}
//checks to see if an alias should be added to the blacklist based on usage. For example, Player 1 is overused and will not be added, or if a name is being impostered
function check_alias($alias)
{ 
	global $file;
	if(strlen($alias) >= 3) //don't log names 2 or fewer chars
	{
		//call checkuser to see if this name is an impersonation
		$matches = file_get_contents("http://rxtron.com/rxcommand/checkuser-for-script.php?search=".$alias);
		if($matches > 4) //the alias they are using has been used by another ip 5+ times, so this might be an impersonation
		{
			p("...but didn't log since the alias ".$alias." is probably an impersonation");
			return false;
		}
		else //the alias they are using has been used less than 5 times so probably isn't an impersonation target
		{ //therefore it is a new name used by this troll
			p("...the alias ".$alias." was used only $matches times so it is probably bad");
			file_put_contents($file, $alias."\n", FILE_APPEND);
			p("New alias logged (".$alias.") using IP as evidence.");
			return true;
		}
	}
	p("...the alias is 3 or fewer characters so didn't log check it.");
	return false;
}
function is_proxy($ip, $tries = 0)
{
	$result = json_decode(file_get_contents("http://ipinfo.io/".$ip));
	if($result->{"country"} == "A1")
	{
		p("IP $ip is a known proxy with country code A1");
		return true; //if country code is A1 it is a registered proxy
	}
	else //need more extensive tests for unregistered proxies
	{
		$url = 'http://www.checkingtools.com/ip_check';
		$vars = 'ip=' . $ip . '&GO!=GO!';
		$ch = curl_init( $url );
		curl_setopt( $ch, CURLOPT_POST, 1);
		curl_setopt( $ch, CURLOPT_POSTFIELDS, $vars);
		curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1);
		curl_setopt( $ch, CURLOPT_HEADER, 0);
		curl_setopt( $ch, CURLOPT_RETURNTRANSFER, 1);

		$response = curl_exec( $ch );
		$triggers = substr_count($response, 'a class="red"');
		if($triggers > 0)
		{
			if(!empty($response)) {
				p("IP $ip has triggered $triggers proxy filters.");
				return falsepcheck($ip);
			}
		}
		else if(empty($response) && $tries < 1)
			return is_proxy($ip, ++$tries); //in case of time-out
		else if(empty($response) || strlen($response) < 50)
			cout("BOT: Proxy check failure: Timeout");
		else
			return false;
	}
	return false;
}
function ban($name, $ip, $wasproxy = false)
{
	global $file, $ban_msg, $proxy_msg, $ban_time;
	$lines = file($file);
	foreach($lines as $l)
	{
		$t = explode(" ", $l);
		if(strtoupper($t[0]) == "WHITELIST" && strpos($t[1], $ip) !== false )
		{
			p("Would have banned $name but ".trim($t[1])." is on the whitelist.");
			return;
		}
	}
	if(is_numeric($name))
	{
		echo "ban_ip $ip $ban_time $ban_msg \n";
		echo "delay_command 3 ban_ip $ip $ban_time $ban_msg\n";
	}
	else {
		if(!$wasproxy) echo "ban $name $ban_time $ban_msg \n";
		else echo "ban $name $ban_time $proxy_msg \n";
	}
   if(!$wasproxy)	cout("0xff8080$name has been auto-banned for ban evasion");	
   else cout("0xff8080$name has been auto-banned for using a proxy.");	
}
function falsepcheck($ip)
{
	global $path, $directory;
	$seg = explode(".",$ip);
	$seg = $seg[0].".".$seg[1]; //partial ip for dynamic ips
	$matches = file_get_contents("http://rxtron.com/rxcommand/checkuser-for-script.php?search=".$seg);
	if($directory != "df") //let's also check your logs
		$matches += sizeof(explode(PHP_EOL, `grep $seg $path`));
	if($matches >= 5) //similar IPs have also played in this server
	{
		p("There are $matches entries/renames with similar ips of $seg, so probably not a proxy");
		return false;
	}
	//another test for name usage for this particular ip
	$grep = `curl -s -o - http://rxtron.com/rxcommand/ips.txt | grep $ip`;
	if($directory != "df")
		$grep .= file_get_contents($path);
	$ra = explode(PHP_EOL, $grep);
	foreach($ra as $a)
	{
		$raa = explode(" ", $a);
		if($raa[0] == 'PLAYER_ENTERED')
			$names[] = normal_chars($raa[1]);
		else if($raa[0] == 'PLAYER_RENAMED')
			$names[] = normal_chars($raa[2]);
	}
	$size = sizeof($names);
	if($size > 14) {
		p("There are $size entries which is > 14 so probably not a proxy");
		return false; //lots of names for 1 ip so most likely not proxy
	}
	else if($size <= 2) {
		p("There are $size entries which is < 3 so it's probably a proxy");
		return true; //only 2 or fewer so it probably is
	}
	$names = array_count_values($names);
	sort($names, SORT_NUMERIC);
	$names = array_reverse($names);
        if(empty($names[1])) $names[1] = 1;
	if($names[0] + max(1,$names[1]) > 3) {
		p("The most used two aliases were used {$names[0]} + {$names[1]}/1 > 4 so not proxy");
		return false;
	}
	else {
		p("The most used two aliases were used ".($names[0] + $names[1])." which is <= 3 so it's probably a proxy");
		return true;
	}
}
function normal_chars($string) //purpose is to combine aliases which use weird ascii with non ascii counterparts
{
	$string = str_replace('\\', '', $string);
	$string = htmlentities($string, ENT_QUOTES, 'UTF-8');
	$string = preg_replace('~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i', '$1', $string);
	$string = html_entity_decode($string, ENT_QUOTES, 'UTF-8');
	$string = preg_replace(array('~[^0-9a-z]~i', '~[ -]+~'), ' ', $string);

	return str_replace(' ', '', $string);
}
function cout($str)
{
	echo "console_message {$str}\n";
}
function p($str)
{
	echo $str."\n";
}
?>