<?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;
}
?>