All pastes #829396 Raw Edit

Stuff

public text v1 · immutable
#829396 ·published 2007-12-23 03:28 UTC
rendered paste body
<?php

$IRCServer        = "irc.freenode.net";
$IRCPort          = "6667";
$BotPrefix        = "FloodBot";
$BotIdent         = "floodbot";
$BotGECOS         = "#ubuntu guard bot";

$NickServPwd      = "";

$WatchedChan      = "#ubuntu";
$RelayChan        = "#ubuntu-ops-monitor";
$OverflowChan     = "#ubuntu-unregged";
$OverflowMessage  = "Please register to join $WatchedChan - Instructions at http://freenode.net/faq.shtml#nicksetup";

$CheckingInterval = 60;
$MaxRoundtrip     = 17;
$FleetSize        = 3;
$Threshold        = 1000;
$Offset           = 13;
$GrowTolerance    = 4;
$ShrinkTolerance  = 7;
$ShrinkCap        = 6;
$SplitThreshold   = 100;

$ThrottlingQuota  = 6;
$ExcessFloodQuota = 1500;


// 22/12/2007
// * When -r or -R was set by another bot, the bot is deopped, in order to avoid -rR flooding (bugfix)
// * !ops call is sent to the slow queue, to improve active response time to attacks
// * +o is now requested during an attack even at times when it should already be set, to be sure it is
// * CTCP ACTION senders are now never banned, to avoid the risk of banning legitimate users
// * Number of QUITs needed to trigger a netsplit response increased to 100
// * +m is now set if an attack is still ongoing 5 seconds after +rR was set
// * Warning for clones is now only given if there are 4 or more of them, rather than just 3
//
// 21/12/2007
//
// * When -R or -r is set by anyone, all opped bots echo the same MODE, to help against server desync
// * On attacks, not only attempt to set +rR and then obtain +o, but also set +rR after obtaining +o
//   (this will help achieve faster response on attacks if the currently opped bot is lagging)
// * Bans are now flushed after less than 1 second, not after less than 2, even with less than 4 bans.
// * Presence of DCC exploits is now checked in QUIT and PART messages as well as PRIVMSG and NOTICE
// * +J/+l is not released on mass-quit if an attack is in progress (they're probably just victims)
//
// 19/12/2007
// * LastEventTime is now changed after each event (bugfix)
// * A netsplit warning is hopefully actually sent on netsplits (bugfix)
// * Mass joins are now reacted to actively (by setting +rR)
// * Slightly stricter checks on PONG to determine if a flood attack is in progress
// * Flood trigger threshold increased to 20 again to avoid nasty false positives
//
// 14/12/2007
// * Warning about clones are now not given if the nicknames are the same except for
//   trailing/leading non-alphabetic characters
//
// 11/12/2007
// * Reduced flood trigger threshold from 20 to 15
// * ACTION is now potentially a CTCP attack, but it takes twice the amount as other CTCPs
// * It now takes 4 CTCPs (or 8 ACTIONs) to trigger an attack response, rather than 3
// * Bans are now flushed only seconds after being added, even if fewer than 4 are queued
// * +J is now sent via the slow queue, hopefully this won't have too many side effects
//
// 10/12/2007
// * Only enforce +J when +l would previously have been enforced
//
// 5/12/2007
// * Attack time is only reset to "now" if an attack isn't already ongoing (bugfix)
// * +o is only requested from ChanServ if an attack isn't already in progress
// * An attack is now considered finished if the bot is deopped
// * Attackers are now banned by hostname
// * People aren't kicked unless they are in the channel
// * REMOVED LIMIT CODE, as +f does not work with +l, and replaced with +J code
// * Offset increased to 13, as mass joins still get detected
// * Clone detection (3 clones cause a warning in the relay channel) added
// * "!netsplit" threshold increased
//
// 4/12/2007
// * If a +l is actually set by someone in the channel, that's now used as the current limit
// * Limit is now never released if an attack is in progress (bugfix)
//
// 3/12/2007
// * Hacked in some real QUIT message based netsplit detection
// * Removed some redundant string matchings in command parsing
// * Raised offset from 11 to 12 as mass joins were too frequent
//
// 2/12/2007
// * MODE -rR is now only sent if MODE +rR was previously set by the bot
// * NICK changes (which are not channel-specific) now contribute to flood count
// * StopAttack changed to make the +rR time window randomized, and longer on repeated attacks
// * Setting +i in the overflow channel resets the last attack time, stopping the overflow messages
// * The overflow messages are sent for a longer time (15 times as long as the checking interval)


error_reporting(E_ALL);
ini_set('display_errors', '1');

declare(ticks=1);
pcntl_signal(SIGTERM, "Terminate", false);
pcntl_signal(SIGINT, "Terminate", false);

Initialize();

function Initialize($PreferredNickname=false) {
	global $IRCServer, $IRCPort, $IRCd;
	global $BotPrefix, $BotNickname, $MonitoredNicks, $BanQueue;
	global $BotIdent, $BotGECOS, $NickServPwd;
	global $FleetSize;
	global $WatchedChan, $OverflowChan, $RelayChan;
	global $UsersCount, $Limit, $PingReplies, $CTCPCount, $FloodCount, $SentBytes;
	global $EnforcingLimit, $Emergency, $ChanServAlive, $Attack, $StoppingAttack, $AttemptingRelease;
	global $LastLimitChangeTime, $LastPing, $LastPong, $LastEventTime, $ExploitTime, $AttackTime;

	if(isset($IRCd)) {
		fwrite($IRCd, "MODE $WatchedChan -lJrRmi\n");
		fwrite($IRCd, "QUIT :Internal error\n");
		fflush($IRCd);
		sleep(5); fclose($IRCd); sleep(20);
	}

	PrintLog("INIT: Connecting to $IRCServer".($PreferredNickname?" using nickname $PreferredNickname":""));

	// Connect to the IRC server
	$IRCd=stream_socket_client($IRCServer.":".$IRCPort);
	if(!$IRCd) {
		PrintLog("INIT: Could not connect to $IRCServer");
		Initialize();
	}
	stream_set_timeout($IRCd, 60);

	SendCmd("USER", "$BotIdent $BotIdent $BotIdent :$BotGECOS");

	// Find a nickname to use
	$BotNickname=SelectNickname($BotPrefix, $PreferredNickname);
	if($PreferredNickname And $BotNickname!=$PreferredNickname) {
		PrintLog("INIT: Trying to reclaim nickname $PreferredNickname");
		SendMsg('NickServ', "GHOST $PreferredNickname $NickServPwd", false, false);
		sleep(5);
		$BotNickname=SelectNickname($BotPrefix, $PreferredNickname);
	}

	// Store the nicknames of bots to monitor
	$MonitoredNicks=array('ChanServ');
	for($i=1; $i<=$FleetSize; $i++) if($BotPrefix.$i!=$BotNickname) array_push($MonitoredNicks, $BotPrefix.$i);

	// Register to services
	if(isset($NickServPwd)) {
		SendCmd("PASS", $NickServPwd, "", false, false);
		SendMsg("NickServ", "IDENTIFY $NickServPwd", false, false);
	}
	sleep(10);

	SendCmd("JOIN", $RelayChan);
	SendCmd("JOIN", $WatchedChan);
	SendCmd("JOIN", $OverflowChan);

	WhoChannel();
	SendMsg($RelayChan, "Synced to $WatchedChan, $UsersCount users present");

	$LastLimitChangeTime=$LastPong=$LastPing=$LastEventTime=time();
	$AttackTime=$ExploitTime=0; $BanQueue=array();
	$Limit=0; $PingReplies=0; $FloodCount=0; $CTCPCount=0; $SentBytes=0;
	$EnforcingLimit=true; $ChanServAlive=false; $Emergency=$Attack=$StoppingAttack=$AttemptingRelease=false;

	ChangeLimit();
	ReleaseLimit();

	stream_set_timeout($IRCd, 5);
}

while(true) {
	// Reconnect if we've been disconnected
	if(!is_resource($IRCd) Or feof($IRCd)) Initialize($BotNickname);

	// Receive from server
	$FromServer=fgets($IRCd);
	if(trim($FromServer)!="" And substr($FromServer, -1)!="\n") {
		SendMsg($RelayChan, "EXCEPTION: Line without linefeed ($FromServer)");
		PrintLog("EXCEPTION: Line without linefeed ($FromServer)");
	}
	$FromServer=trim($FromServer);

	// Draw commands to send from the send queue, remove from queue if someone else executes them
	SendCmd($FromServer);

	// Flush any pending bans
	FlushBans();

        // If we have has many (or more?!) users than we have set a limit for, we have a problem
        if($UsersCount>=$Limit) {
		if(!$Emergency) {
	                StopAttack("mass join", true);
	                if($UsersCount>$Limit) PrintLog("EXCEPTION: more users in channel ($UsersCount) than limit allows ($Limit)");
		}
        }

        // We want to react on netsplits and such network issues by setting a failsafe -l
        if(abs($UsersCount-$Limit)>$SplitThreshold) {
                if(!$Emergency) SendMsg($WatchedChan, "!netsplit");
                if(!$StoppingAttack) ReleaseLimit();
        }

        // If we don't get timely PING replied from the monitored nicknames, we set a failsafe -l
        if($LastPong>$LastPing+$MaxRoundtrip Or (time()>$LastPing+$MaxRoundtrip And $LastPing>$LastPong)) {
                if(!$Emergency) {
			if($ChanServAlive) PrintLog("WARN: Excessive lag detected on other bots, removing limit");
			else SendMsg($RelayChan, "WARNING: ChanServ is not replying, removing limit", true);
		}
                ReleaseLimit();
        }

        // Commands are in the form :<nickname>!<ident>@<hostmask> <command> [<target>] :<body>
        if($FromServer!="" And eregi(':([^ .!]+)([^ ]+) +([^ ]+) +(:(.*)|([^ ]+) +:?(.*))$', $FromServer, $Parts)) {
                $Nick=$Parts[1]; $Mask=$Parts[2]; $Command=$Parts[3]; $Target=$Parts[6]; $Body=$Parts[7]; $ShortBody=$Parts[5];
		$Host=substr($Mask, strpos($Mask, "@")+1);
		if($Command!="QUIT" And $Command!="JOIN" And ($Command=="NICK" Or Match($Target, $WatchedChan))) {
			if(time()-$LastEventTime<1) {
				$FloodCount+=1;
				if($FloodCount==20) SendCmd("PING", ":".$IRCServer);
			} else {
				$FloodCount-=2;
				if($FloodCount<0 Or $FloodCount+2>20) $FloodCount=0;
			}
			$LastEventTime=time();
		}
                if($Command=="PRIVMSG" Or $Command=="NOTICE") {
			if(Match($Target, $WatchedChan)) {
				HandleExploit($Nick, $Body);
				if($Command=="NOTICE" Or substr($Body, 0, 1)=="\1") {
		                        if(ereg("^\1ACTION", $Body)) $CTCPCount+=0.5; else {
						$CTCPCount=floor($CTCPCount+1);
						if($CTCPCount==1) SendMsg($RelayChan, "WARNING: $Nick CTCP/NOTICE'd $WatchedChan ($Body)", true);
						if($CTCPCount>=4) {
							if($CTCPCount==4) StopAttack("CTCP/NOTICE");
							AddBan("*!*@".$Host);
						}
					}
				}
			}
			if($Command=="PRIVMSG") {
	                        if(Match($Target, $BotNickname) And in_array($Nick, $MonitoredNicks) And ereg("\1PING", $Body)) {
					// Reply to CTCP PING from other bot
					SendCmd("NOTICE", $Nick, ":".$Body);
				} else if(Match($Target, $RelayChan)) {
					if($Body=="status") {
						SendMsg($RelayChan,
							"$UsersCount users - limit $Limit"
							.(!$EnforcingLimit?" not enforced":"")
							.($Emergency?" - emergency mode":"")
							.($GraceRound?" - grace round":"")
							.($Attack?" - attack ongoing":"")
						);
       	                         	} else if($Body=="sync") {
						WhoChannel();
						SendMsg($RelayChan, "Synced to $WatchedChan, $UsersCount users present");
					} else if($Body=="hook") {
						$Limit=$UsersCount+$Offset;
						SendMsg($RelayChan, "Limit will be enforced at the next non-emergency round, see 'status'");
					} else if($Body=="clones") {
						asort($Who); $Clones=array(); $List=array(); $PrevHost=false;
						foreach($Who as $Nick => $Host) {
							if($Host==$PrevHost) {
								array_push($Clones, $Nick);
							} else {
								sort($Clones);
								if(count($Clones)>1) array_push($List, $PrevHost.": ".implode(", ", $Clones));
								$Clones=array($Nick);
							}
							$PrevHost=$Host;
						}
						shuffle($List); foreach($List as $Message) SendMsg($RelayChan, $Message, true);

//						$List=array(); foreach($Who as $Host) {
//			                                $Clones=array_keys($Who, $Host);
//							if(count($Clones)>=2) array_push($List, $Host.": ".implode(", ", $Clones));
//						}
//						$List=array_unique($List); sort($List);
//						while(count($List)>0) SendMsg($RelayChan, implode(" || ", array_splice($List, 0, 6)), true);
					}
				}
			} else if($Command=="NOTICE") {
		        	// Check CTCP PING replies from ChanServ and other watched bots
		                if(Match($Nick, 'ChanServ')) $ChanServAlive=true;
		                if(in_array($Nick, $MonitoredNicks) And eregi("^\1PING ".$LastPing, $Body)) {
		                	PrintLog("RECV: $Nick replied after ".(time()-$LastPing)." s");
		                        if(!Match($Nick, 'ChanServ')) $PingReplies+=1;
		                        // If enough people replied to PING, we are fine
		                        if($PingReplies>=2 And $ChanServAlive) {
		                        	$LastPong=time();
		                                PrintLog("STAT: Everybody replied after ".($LastPong-$LastPing)." s");
		                        }
		                } else {
		                        PrintLog("RECV: $Body from $Nick. but PING seed was $LastPing");
				}
                        }
		} else if($Command=="JOIN") {
			if(Match($Body, $WatchedChan) Or Match($ShortBody, $WatchedChan) Or Match($Target, $WatchedChan)) {
				$Who[$Nick]=$Host; $UsersCount+=1;
				$Clones=array_keys($Who, $Host);
				foreach($Clones as $Key => $Clone) $Clones[$Key]=trim($Clones[$Key], '-_`|^0123456789');
				$Clones=array_unique($Clones);
				if(count($Clones)>=4) {
					SendCmd("PING", $IRCServer, "", true);
					SendMsg($RelayChan, "WARNING: ".implode(", ", $Clones)." are all connected from ".$Host, true);
				}
				PrintLog("JOIN: $Nick ($UsersCount)");
			} else PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)");
                } else if($Command=="PART") {
			HandleExploit($Nick, $Body);
			if(Match($ShortBody, $WatchedChan) Or Match($Target, $WatchedChan)) {
				unset($Who[$Nick]); $UsersCount-=1;
				PrintLog("PART: $Nick ($UsersCount)");
			} else ("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)");
		} else if($Command=="KICK") {
			if(Match($Target, $WatchedChan)) {
				$Victim=substr($Body, 0, strpos($Body, " "));
				unset($Who[$Victim]); $UsersCount-=1;
				PrintLog("KICK: $Victim by $Nick ($UsersCount)");
			}
                } else if($Command=="QUIT") {
			HandleExploit($Nick, $ShortBody);
			if(isset($Who[$Nick])) {
				unset($Who[$Nick]); $UsersCount-=1;
				PrintLog("QUIT: $Nick ($UsersCount)");
			} else PrintLog("$Nick has left IRC, but was not in $WatchedChan");
                        if(in_array($Nick, $MonitoredNicks) And $ShortBody!='"Maintenance"') {
                                PrintLog("WARN: $Nick has quit IRC, removing limit");
				SendMsg("ChanServ", "OP $WatchedChan");
                                ReleaseLimit();
                        }
			if(eregi("reset by peer", $ShortBody) And time()-$ExploitTime<2) {
				AddBan($Nick."!*@*!#ubuntu-read-topic", "Your router has a bug - Please rejoin and read the channel topic");
			}
                        if(!ereg('"', $ShortBody) And strpos($ShortBody, $IRCServer)!==false) {
                                if(!$Emergency) SendMsg($RelayChan, "WARNING: Network split", true);
				ReleaseLimit();
                        }
		} else if($Command=="NICK") {
			if(isset($Who[$Nick])) {
				unset($Who[$Nick]);
				if(!Match($ShortBody, "")) $Who[$ShortBody]=$Host;
				else if(!Match($Target, "")) $Who[$Target]=$Host;
				else SendMsg($RelayChan, "EXCEPTION: $Nick changed nickname to an unknown nickname");
				PrintLog("NICK: from $Nick to $ShortBody ($UsersCount)");
			} else PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)");
                } else if($Command=="MODE") {
			if(Match($Target, $WatchedChan)) {
				if(ereg('^\+o ([^[:space:]]+)', $Body, $Parts)) {
					$ModeParameter=$Parts[1];
		                	if(Match($Nick, 'ChanServ') And Match($ModeParameter, $BotNickname)) {
			                        // If we've just been opped, that probably means we're in an emergency and -l should be set.
			                        if($AttemptingRelease And !$Attack) ReleaseLimit(false);
						// Or that we're being attacked.
						if($StoppingAttack) SendCmd("MODE", $WatchedChannel, "+rR-z");
					} else if(in_array($ModeParameter, $MonitoredNicks)) {
			                        // If another bot was opped, leave +l alone for a little longer, to avoid MODE spamming
			                        $LastLimitChangeTime+=rand($MaxRoundtrip, $MaxRoundtrip*2);
					}
				} else if(ereg("-o $BotNickname", $Body)) {
					$Attack=false;
				} else if($Body=="-l" Or $Body=="-J") {
					PrintLog("MODE: Emergency release enforced by $Nick");
					$EnforcingLimit=false; $AttemptingRelease=false;
				} else if(ereg('\+[^ ]*J', $Body)) {
					$EnforcingLimit=true;
//					$Limit=intval(substr($Body, strrpos($Body, " ")));
				} else if(ereg('\+[^ ]*(r|R)', $Body) And !Match($Nick, $BotNickname)) {
					PrintLog("MODE: $Nick set $Body, any attack is now considered handled");
					$AttackTime=time(); $StoppingAttack=false;
				} else if(ereg('\-[^ ]*(r|R)', $Body) And !Match($Nick, $BotNickname)) {
					PrintLog("MODE: $Nick set $Body, syncing the same mode on my server");
					SendCmd("MODE", $WatchedChan, $Body, true);
					if(in_array($Nick, $MonitoredNicks)) SendCmd("MODE", $WatchedChan, "-o $Nick");
				}
			} else if(Match($Target, $OverflowChan)) {
				if($Body=="+i") {
					ReleaseLimit(false);
					SendCmd("MODE", $OverflowChan, "+if $WatchedChan", true);
					SendCmd("NAMES", $OverflowChan, "", true);
					$AttackTime=0;
				}
			}
		} else if($Command=="PONG") {
			if($FloodCount>=20 And time()-$LastEventTime<3 And $SentBytes<$ExcessFloodQuota) StopAttack("flood");
			else $SentBytes=floor($SentBytes/4);
			$FloodCount=0;
		} else if($Command=="324") {
			// This is a MODE reply
			if(ereg(' \+[^ ]*l', $Body)) {
				if(!$EnforcingLimit) SendCmd("MODE", $OverflowChan, "-i", true);
				$EnforcingLimit=true;
			} else {
				$EnforcingLimit=false;
			}
		} else if($Command=="353" And !$Attack) {
			// This is a NAMES reply for the overflow channel
			$Body=substr($Body, strpos($Body, ":")+1); $Names=explode(" ", $Body);
			// Load balance kicking among the bots (although all the kicks are still sent to the server)
			shuffle($Names); foreach($Names as $Name) {
				if(substr($Name, 0, 1)!="@") SendCmd("KICK", $OverflowChan, "$Name :Please join $WatchedChan", true);
			}
		} else if($Command=="366") {
			// End of NAMES list
			SendCmd("MODE", $OverflowChan, "-i", true);
                } else if($Command=="NOTICE") {
	                // Check CTCP PING replies from ChanServ and other watched bots
			if(Match($Nick, 'ChanServ')) $ChanServAlive=true;
	                if(in_array($Nick, $MonitoredNicks) And eregi("^\1PING ".$LastPing, $Body)) {
	                        PrintLog("RECV: $Nick replied after ".(time()-$LastPing)." s");
	                        if(!Match($Nick, 'ChanServ')) $PingReplies+=1;
				// If enough people replied to PING, we are fine
	                        if($PingReplies>=2 And $ChanServAlive) {
					$LastPong=time();
					PrintLog("STAT: Everybody replied after ".($LastPong-$LastPing)." s");
				}
                        } else {
				PrintLog("Received $Body from $Nick but seed was $LastPing");
			}
                } else if($Command=="319") {
			// This is a WHOIS reply
			$Bot=substr($Body, 0, strpos($Body, ' '));
			if(!Match('ChanServ', $Bot) And !Match($BotNickname, $Bot)) {
				if(ereg('@'.$WatchedChan.'([[:space:]]|$)', $Body)) SendCmd("MODE", $WatchedChan, "-o $Bot", true);
			}
		} else if($Command!="311" And $Command!="312" And $Command!="318" And $Command!="320" And $Command!="482") {
			PrintLog("UNEXPECTED: $Command from $Nick to $Target - $Body - $ShortBody ($FromServer)");
		}
        }

        // Execute the periodic actions
        if(time()-$LastLimitChangeTime>$CheckingInterval) {
		// Send pings to ChanServ and other bots to make sure they're all alive and well
                $ChanServAlive=false; $PingReplies=0; $LastPing=time();
		foreach($MonitoredNicks as $Nick) SendMsg($Nick, "\1PING ".$LastPing."\1", false);
                // WHOIS other bots to see if they're opped, but only do it half of the times, no need to flood
                foreach($MonitoredNicks as $Bot) if($Bot!='ChanServ' And rand(0,1)==1) SendCmd("WHOIS", $Bot, "", true);
                // Do some emergency cleanup operations
                if(time()-$AttackTime<$CheckingInterval*15) if(rand(0, $FleetSize/2)==0) SendMsg($OverflowChan, $OverflowMessage, true);
                if($StoppingAttack) SendCmd("MODE", $WatchedChan, "-rRmi", true);
		FlushBans(); $CTCPCount=0; $FloodCount=0; $Attack=$StoppingAttack=false; $SentBytes=floor($SentBytes/4);
                // Change the limit if necessary
                ChangeLimit();
		PrintLog("STAT: $UsersCount users, limit $Limit, diff ".($Limit-$UsersCount));
                // Remember when we last did all this
                $LastLimitChangeTime=time();
        }
}


function ReleaseLimit($Force=true) {
        global $WatchedChan;
        global $Emergency, $EnforcingLimit, $GraceRound, $AttemptingRelease;

	if($EnforcingLimit) {
		if(!$Force Or !$GraceRound) SendCmd("MODE", $WatchedChan, "-J");
                if($Force And !$GraceRound) SendMsg("ChanServ", "OP $WatchedChan");
		$AttemptingRelease=true;
        }
	$Emergency=true;
	$GraceRound=true;
}

function ChangeLimit() {
	global $UsersCount;
	global $Limit, $Threshold, $Offset, $GrowTolerance, $ShrinkTolerance, $ShrinkCap;
	global $WatchedChan, $OverflowChan;
	global $Emergency, $EnforcingLimit, $GraceRound;

	$NewLimit=$UsersCount+$Offset;
        // Never go below a defined threshold
        if($NewLimit<$Threshold) $NewLimit=$Threshold;
	// Only act if the limit should be changed by more than just one unit (or two on decreasing)
	if(!$GraceRound) {
		if(rand(0, 15)==0 Or $NewLimit>$Limit+$GrowTolerance Or $NewLimit<$Limit-$ShrinkTolerance Or !$EnforcingLimit) {
	                if(!$EnforcingLimit) {
	                        SendCmd("MODE", $OverflowChan, "-i", true);
	                        SendCmd("MODE", $WatchedChan, "+J 2,5", true);
	                }
			// If many users have parted, decrease the limit cautiously, as it might be a netsplit
			if($NewLimit<$Limit-$ShrinkCap) $NewLimit=$Limit-$ShrinkCap;
			if($NewLimit>$Limit+$Threshold) $NewLimit=$Limit+$Threshold;
			$Limit=$NewLimit;
//			SendCmd("MODE", $WatchedChan, "+l $Limit");
		}
		$Emergency=false;
		$EnforcingLimit=true;
	} else {
		$Limit=max($Limit-round($ShrinkCap/3), $UsersCount+($Offset+$ShrinkTolerance*2)-1, $Threshold);
		PrintLog("STAT: Limit ($Limit) is currently not enforced");
		$GraceRound=false;
	}
}

function SelectNickname($BotPrefix, $PreferredNickname=false) {
	global $IRCd;
        // Find a nickname to use
        $BotNumber=1;
	if($PreferredNickname) $BotNickname=$PreferredNickname;
	else $BotNickname=$BotPrefix.$BotNumber;
	SendCmd("NICK", $BotNickname);
        for($i=0; $i<6; $i++) {
                if(ereg(":[^[:space:]]+ 433", fgets($IRCd))) {
                        $BotNumber+=1; $i=0; $BotNickname=$BotPrefix.$BotNumber;
                        PrintLog("INIT: Nickname is already in use, trying ".$BotNickname);
                        SendCmd("NICK", $BotNickname);
                }
        }
        return $BotNickname;
}

function SendMsg($Target, $Body, $Slow=false, $Log=true) {
	SendCmd('PRIVMSG', $Target, ":".$Body, $Slow, $Log);
}

function SendCmd($Command="", $Target="", $Body="", $Slow=false, $Log=true) {
	global $IRCd, $IRCServer, $BotNickname;
	global $SentBytes, $ExcessFloodQuota, $ThrottlingQuota;
	static $SlowQueue=array();
	static $FastQueue=array();
	static $LastSendTime=0;
	static $StackedSends=-30;

	if($Command=="USER") $StackedSends=-30;

	$CurrentTime=time();
	if($CurrentTime-$LastSendTime>3) $StackedSends=0;
	if($CurrentTime-$LastSendTime>1) $StackedSends-=1;
	if($StackedSends<0) $StackedSends+=1;

	if(trim($Command)=="") $Command=":";
	if(substr($Command, 0, 1)==":") {
		// If someone else already did something, we don't need to replicate it
		$SlowCount=count($SlowQueue); $FastCount=count($FastQueue);
		$SlowQueue=array_diff($SlowQueue, array(substr($Command, strpos($Command, " ")+1)));
		$FastQueue=array_diff($FastQueue, array(substr($Command, strpos($Command, " ")+1)));
		if(count($SlowQueue)!=$SlowCount) PrintLog("FIFO: Remove duplicate (slow) $Command");
		if(count($FastQueue)!=$FastCount) PrintLog("FIFO: Remove duplicate (fast) $Command");
		if($CurrentTime-$LastSendTime>=1+rand(0, 1)) {
			// If there is a message in the prioritized queue, pick it
			$CurrentMsg=array_shift($FastQueue);
			// Otherwise, pick a message from the slow queue
			if($CurrentMsg==NULL) $CurrentMsg=array_shift($SlowQueue);
			// If there's nothing at all to send, return
			if($CurrentMsg==NULL) return;
			PrintLog("FIFO: Shift $CurrentMsg");
		} else return;
	} else {
		$CurrentMsg=substr("$Command $Target ".$Body, 0, 450);
	}
	if($Slow Or $StackedSends>=$ThrottlingQuota Or $SentBytes>=$ExcessFloodQuota) {
		PrintLog("FIFO: Push with".count($SlowQueue)." slow, ".count($FastQueue)." fast ".($Slow?"(Slow) ":"(Fast) ").$CurrentMsg);
		// Even when throttling everything to avoid Excess Flood, priorities are considered
		if($Slow) {
			array_push($SlowQueue, $CurrentMsg);
		} else {
			array_push($FastQueue, $CurrentMsg);
		}
	} else {
                // If we're flooding too much, wait for a PING reply before going on
		if($CurrentTime-$LastSendTime<1) $StackedSends+=1;
                $SentBytes+=strlen($CurrentMsg); $LastSendTime=$CurrentTime;
                if($SentBytes>=$ExcessFloodQuota) {
			if(false===fwrite($IRCd, "PING :$IRCServer\n")) Initialize($BotNickname);
			PrintLog("WARN: Flood quota reached, sending server PING");
		} else {
			if(false===fwrite($IRCd, $CurrentMsg."\n")) Initialize($BotNickname);
			if($Log) PrintLog("SENT: ".$CurrentMsg);
		}
	}
}

function WhoChannel() {
        global $IRCd, $Who, $UsersCount;
        global $WatchedChan, $RelayChan;
        $UsersCount=0;
        SendCmd("WHO", $WatchedChan);
        while($Response=fgets($IRCd)) {
                if(ereg('^:[^[:space:]]+ +352', $Response)) {
			$Parts=explode(" ", $Response);
                        if(Match($Parts[3], $WatchedChan)) {
				$Who[$Parts[7]]=$Parts[5];
				$UsersCount+=1;
			}
                } else {
                        if($UsersCount==0) continue;
                        if(!ereg('^:[^[:space:]]+ 315', $Response)) {
                                PrintLog("WHO without end of /WHO");
                                SendMsg($RelayChan, "EXCEPTION: WHO without end of /WHO");
                                Initialize();
                        }
                        break;
                }
        }
}

function AddBan($BanMask, $Message=false) {
	global $BanQueue;
	static $BanMemory=array();
	if(count($BanMemory)>50) $BanMemory=array();
	if(!in_array($BanMask, $BanMemory)) {
		array_push($BanQueue, $BanMask);
                array_push($BanMemory, $BanMask);
		PrintLog("Banning: $BanMask ($Message)");
	}
        if(count($BanQueue)>=4) FlushBans($Message);
}

function FlushBans($KickMessage=false) {
	global $Who;
	global $BanQueue;
	global $WatchedChan;
	static $Message=false;
	static $LastFlushTime=0;
	if(time()-$LastFlushTime<1 And count($BanQueue)<4) return;
	if($KickMessage) $Message=$KickMessage;
	$Bans=array_slice($BanQueue, 0, min(4, count($BanQueue)));
        if(count($Bans)>0) SendCmd("MODE", $WatchedChan, '+'.str_repeat("b", count($Bans))." ".implode(" ", $Bans));
	foreach($Bans as $Mask) {
		array_pop($BanQueue);
		$Nick=substr($Mask, 0, strpos($Mask, "!"));
		if($Message And isset($Who[$Nick])) SendCmd("KICK", $WatchedChan, $Nick." :".$Message, true);
	}
	if(!$KickMessage) $Message=false;
	$LastFlushTime=time();
}

function StopAttack($AttackDescription="attack", $Actively=true) {
	global $WatchedChan, $RelayChan, $OverflowChan;
	global $LastLimitChangeTime, $CheckingInterval, $MaxRoundtrip, $AttackTime;
	global $Emergency, $Attack, $StoppingAttack;
	if($Actively And !$StoppingAttack) SendCmd("MODE", $WatchedChan, "+rR-z");
	if(!$Attack Or !$StoppingAttack) {
		SendMsg($RelayChan, "!ops | $AttackDescription", true);
		SendCmd("MODE", $OverflowChan, "-i", true);
		$MaxMultiplier=($CheckingInterval/(abs(time()-$AttackTime)+1))*4;
		$LastLimitChangeTime=time()-$CheckingInterval+$MaxRoundtrip*rand(2, 3+$MaxMultiplier);
		if(abs($LastLimitChangeTime-time())>120) {
			SendMsg($RelayChan, "EXCEPTION: Next round is more than 2 minutes from now", true);
			$LastLimitChangeTime=time()+120;
		}
		SendMsg('ChanServ', "OP $WatchedChan");
		$AttackTime=time();
	}
	if($Actively) {
		if($StoppingAttack And time()-$AttackTime>5) {
			SendCmd("MODE", $WatchedChan, "+m");
			$LastLimitChangeTime=time()-$CheckingInterval+int($MaxRoundtrip/2);
		}
		$StoppingAttack=true;
	}
	FlushBans();
        $Emergency=true; $Attack=true;
	PrintLog("RECV: $AttackDescription attack".($Actively?", stopping actively":""));
}

function HandleExploit($Nick, $Body) {
	global $ExploitTime;
	if(ereg("DCC (SEND)|(CHAT) [^[:space:]]{14}", $Body)) {
        	PrintLog("RECV: DCC exploit attempted by $Nick");
                StopAttack("exploit", false);
                $ExploitTime=time();
        }
}


function Terminate($Signal) {
	global $IRCd;
	global $WatchedChan;
//	fwrite($IRCd, "MODE $WatchedChan -l\n");
	fwrite($IRCd, "QUIT :Maintenance\n");
	fflush($IRCd);
	sleep(4);
        $Sockets=array($IRCd);
        stream_select($Sockets, $Write=NULL, $Except=NULL, 10);
	sleep(4);
	fclose($IRCd);
	exit(0);
}	

function PrintLog($Text) {
	print date("[d/m/y H:i:s] ", time()).$Text."\n";
}

function Match($String1, $String2) {
	if(strtolower(trim($String1))==strtolower(trim($String2))) return true;
	else return false;
}

?>